Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 11 additions & 110 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import {themes as prismThemes} from "prism-react-renderer";
const path = require("path");
const remarkFaqSchema = require("./src/remark/remarkFaqSchema");
const {siteGraph} = require("./src/schema/siteEntities");
const fs = require("fs");
import {visit} from "unist-util-visit";
const FontPreloadPlugin = require("webpack-font-preload-plugin");
Expand Down Expand Up @@ -60,121 +62,17 @@ module.exports = {
href: "https://keploy.io/",
},
},
// Site-wide entity graph (Organization, WebSite, SoftwareApplication).
// These used to be three sibling <script> blocks, each re-declaring its
// own publisher, which left several disconnected Organization nodes on
// every page. They now share one graph and are addressable by `@id`, so
// per-page schema can point at them instead of copying them.
{
tagName: "script",
attributes: {
type: "application/ld+json",
},
innerHTML: JSON.stringify({
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "Keploy",
description:
"Keploy is an open-source, AI-powered testing agent and sandboxing platform that automatically generates test cases, dependency mocks, and production-like sandboxes from real user traffic using eBPF. It helps developers achieve 90% test coverage in minutes with zero code changes. Native support is available on Linux; macOS and Windows require a Linux environment such as Lima, WSL, or Docker.",
applicationCategory: "DeveloperTool",
applicationSubCategory: "Test Automation",
operatingSystem: "Linux",
license: "https://www.apache.org/licenses/LICENSE-2.0",
softwareHelp: "https://keploy.io/docs/",
codeRepository: "https://github.com/keploy/keploy",
downloadUrl: "https://github.com/keploy/keploy/releases",
isAccessibleForFree: true,
url: "https://keploy.io",
featureList: [
"Automatic test case generation from real user traffic",
"Production-like sandbox environments from captured traffic",
"AI-powered dependency virtualization and mock generation",
"Record and replay testing with eBPF kernel capture",
"AI noise detection for flaky test elimination",
"Legacy application testing without code changes",
"Migration regression testing against production baselines",
"Continuous validation in CI/CD pipelines",
"Multi-language support (Go, Java, TypeScript, Python)",
],
keywords: [
"test automation",
"API testing",
"API test generation",
"unit testing",
"integration testing",
"mock generation",
"dependency virtualization",
"eBPF-based testing",
"record and replay",
"production sandbox",
],
programmingLanguage: [
"Go",
"Java",
"TypeScript",
"JavaScript",
"Python",
],
publisher: {
"@type": "Organization",
name: "Keploy",
url: "https://keploy.io",
},
}),
},
{
tagName: "script",
attributes: {
type: "application/ld+json",
},
innerHTML: JSON.stringify({
"@context": "https://schema.org/",
"@type": "Organization",
name: "Keploy",
url: "https://keploy.io/",
logo: "https://keploy.io/images/keploy-logo-full.svg",
foundingDate: "2021-01-01",
knowsAbout: [
"API Testing",
"Test Automation",
"eBPF-based Testing",
"Dependency Virtualization",
"AI-Powered Testing",
],
award: [
"API World 2023 Award: Best in API Infrastructure",
"CNCF Landscape",
"Google for Startups Accelerator",
"Google Summer of Code Mentoring Organization",
],
sameAs: [
"https://github.com/keploy",
"https://twitter.com/Keployio",
"https://www.linkedin.com/company/keploy",
"https://www.youtube.com/@keploy",
"https://discord.gg/keploy",
"https://community.keploy.io",
"https://marketplace.visualstudio.com/items?itemName=Keploy.keployio",
"https://chromewebstore.google.com/detail/keploy-api-test-recorder/ohcclfkaidblnjnggclkiecgkpgldihe",
"https://www.crunchbase.com/organization/hybridk8s",
"https://www.gartner.com/reviews/product/keploy-618993540",
"https://www.g2.com/products/keploy/reviews",
"https://www.capterra.in/software/1070466/Keploy",
"https://aws.amazon.com/marketplace/reviews/reviews-list/prodview-xgwmdk4ivjjv4",
],
}),
},
{
tagName: "script",
attributes: {
type: "application/ld+json",
},
innerHTML: JSON.stringify({
"@context": "https://schema.org/",
"@type": "WebSite",
name: "Keploy Documentation",
url: "https://keploy.io/docs/",
potentialAction: {
"@type": "SearchAction",
target: "https://keploy.io/docs/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
}),
innerHTML: JSON.stringify(siteGraph),
},
// Meta Pixel Code
{
Expand Down Expand Up @@ -484,6 +382,9 @@ fbq('track', 'PageView');`,
},
{},
],
// Emit FAQPage JSON-LD (Question/Answer) for FAQ docs so they are
// eligible for FAQ rich results and AI extraction.
remarkFaqSchema,
],
},
// Will be passed to @docusaurus/plugin-content-blog
Expand Down
120 changes: 120 additions & 0 deletions scripts/verify-schema-graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* Verifies the JSON-LD graph in the built site.
*
* The @id-based consolidation only pays off if every bare {"@id": "..."}
* reference resolves to a node actually defined on the same page -- an
* unresolved reference is worse than the inline duplicate it replaced,
* because consumers get a dangling pointer instead of an entity. This walks
* the built HTML and fails on unparseable JSON-LD or dangling references.
*
* Usage: node scripts/verify-schema-graph.js [buildDir]
*/
const fs = require("fs");
const path = require("path");

const buildDir = process.argv[2] || "build";
const SKIP_VERSIONS = ["/1.0.0/", "/2.0.0/"];

function findHtml(dir, out = []) {
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
findHtml(full, out);
} else if (entry.name === "index.html") {
out.push(full);
}
}
return out;
}

// The build inlines JSON-LD into <script type="application/ld+json">. Entities
// are HTML-escaped by React, so unescape before parsing.
const SCRIPT_RE =
/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g;

function unescapeHtml(s) {
return s
.replace(/&quot;/g, '"')
.replace(/&#x27;/g, "'")
.replace(/&#39;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&");
}

// Collect every node that declares an @id, and every bare {"@id"} reference.
function walk(node, defined, referenced) {
if (Array.isArray(node)) {
node.forEach((n) => walk(n, defined, referenced));
return;
}
if (!node || typeof node !== "object") {
return;
}
const keys = Object.keys(node).filter((k) => k !== "@context");
if (node["@id"]) {
// A node carrying only "@id" is a reference; anything else defines it.
if (keys.length === 1) {
referenced.add(node["@id"]);
} else {
defined.add(node["@id"]);
}
}
for (const key of keys) {
walk(node[key], defined, referenced);
}
}

const files = findHtml(buildDir).filter(
(f) => !SKIP_VERSIONS.some((v) => f.includes(v))
);

let parseErrors = 0;
let dangling = 0;
let blocks = 0;
const typeCounts = new Map();

for (const file of files) {
const html = fs.readFileSync(file, "utf8");
const defined = new Set();
const referenced = new Set();
let match;
SCRIPT_RE.lastIndex = 0;
while ((match = SCRIPT_RE.exec(html))) {
blocks += 1;
let parsed;
try {
parsed = JSON.parse(unescapeHtml(match[1]));
} catch (err) {
parseErrors += 1;
console.error(`INVALID JSON-LD ${file}\n ${err.message}`);
continue;
}
const graph = parsed["@graph"] || parsed;
walk(graph, defined, referenced);
for (const n of Array.isArray(graph) ? graph : [graph]) {
const t = n && n["@type"];
if (typeof t === "string") {
typeCounts.set(t, (typeCounts.get(t) || 0) + 1);
}
}
}
for (const ref of referenced) {
if (!defined.has(ref)) {
dangling += 1;
console.error(`DANGLING @id ${file}\n ${ref}`);
}
}
}

console.log(`\nPages scanned: ${files.length}`);
console.log(`JSON-LD blocks: ${blocks}`);
console.log(`Invalid JSON: ${parseErrors}`);
console.log(`Dangling @id refs: ${dangling}`);
console.log("\nTop-level @type distribution:");
for (const [type, count] of [...typeCounts].sort((a, b) => b[1] - a[1])) {
console.log(` ${String(count).padStart(5)} ${type}`);
}

process.exit(parseErrors || dangling ? 1 : 0);
55 changes: 41 additions & 14 deletions src/components/GetStartedPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
import {FaRocket, FaMagic, FaRetweet, FaLaptopCode} from "react-icons/fa";
import {SiKubernetes} from "react-icons/si";

// The four entry points rendered as cards below, in display order. Exported
// so the docs home can emit them as an ItemList off the same data the links
// are built from -- a hand-copied list in the JSON-LD would quietly rot the
// first time one of these paths moves.
export const GET_STARTED_PATHS = [
{
name: "Generate from OpenAPI Schema / Postman Collection / cURL",
description:
"Connect resources in Keploy Console and generate validated suites",
href: "/docs/running-keploy/generate-api-tests-using-ai/",
},
{
name: "Record with Chrome extension",
description: "Turn browser actions into API test flows (no mocks)",
href: "/docs/running-keploy/api-testing-chrome-extension/",
},
{
name: "Record in K8s, Replay Anywhere (Local/CI/K8s)",
description: "Capture from staging/prod, replay in your pipeline",
href: "/docs/quickstart/k8s-proxy/",
},
{
name: "Record Locally",
description: "Classic Keploy CLI experience for rapid iteration",
href: "/docs/server/installation/",
},
];

const [AI_GENERATE, CHROME_EXTENSION, K8S_PROXY, LOCAL_RECORD] =
GET_STARTED_PATHS;

export const GetStartedPaths = () => {
return (
<section className="mb-12">
Expand All @@ -24,7 +55,7 @@ export const GetStartedPaths = () => {
{/* Two Paths Grid */}
<div className="grid gap-6 md:grid-cols-2">
{/* AI Test Generation */}
<div className="group relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-white to-orange-50/50 p-6 h-full shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-orange-100/50 dark:border-gray-700 dark:from-gray-800 dark:to-orange-950/20 dark:hover:shadow-orange-900/20">
<div className="group relative h-full overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-white to-orange-50/50 p-6 shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-orange-100/50 dark:border-gray-700 dark:from-gray-800 dark:to-orange-950/20 dark:hover:shadow-orange-900/20">
{/* Decorative gradient blob */}
<div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/10 blur-2xl transition-transform duration-500 group-hover:scale-150"></div>

Expand Down Expand Up @@ -80,10 +111,8 @@ export const GetStartedPaths = () => {
{/* Console Options */}
<div className="mt-auto space-y-3 pt-2">
<Link
to={useBaseUrl(
"/docs/running-keploy/generate-api-tests-using-ai/"
)}
className="group/option flex items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 min-h-[92px] transition-all duration-200 hover:border-orange-300 hover:bg-orange-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-orange-500/50 dark:hover:bg-orange-900/10"
to={useBaseUrl(AI_GENERATE.href)}
className="group/option flex min-h-[92px] items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 transition-all duration-200 hover:border-orange-300 hover:bg-orange-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-orange-500/50 dark:hover:bg-orange-900/10"
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 text-orange-700 transition-colors group-hover/option:bg-orange-100 group-hover/option:text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
Expand All @@ -105,10 +134,8 @@ export const GetStartedPaths = () => {
</Link>

<Link
to={useBaseUrl(
"/docs/running-keploy/api-testing-chrome-extension/"
)}
className="group/option flex items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 min-h-[92px] transition-all duration-200 hover:border-orange-300 hover:bg-orange-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-orange-500/50 dark:hover:bg-orange-900/10"
to={useBaseUrl(CHROME_EXTENSION.href)}
className="group/option flex min-h-[92px] items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 transition-all duration-200 hover:border-orange-300 hover:bg-orange-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-orange-500/50 dark:hover:bg-orange-900/10"
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 text-orange-700 transition-colors group-hover/option:bg-orange-100 group-hover/option:text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
Expand All @@ -132,7 +159,7 @@ export const GetStartedPaths = () => {
</div>

{/* Record & Replay */}
<div className="group relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-white to-purple-50/50 p-6 h-full shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-purple-100/50 dark:border-gray-700 dark:from-gray-800 dark:to-purple-950/20 dark:hover:shadow-purple-900/20">
<div className="group relative h-full overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-white to-purple-50/50 p-6 shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-purple-100/50 dark:border-gray-700 dark:from-gray-800 dark:to-purple-950/20 dark:hover:shadow-purple-900/20">
{/* Decorative gradient blob */}
<div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-gradient-to-br from-purple-400/20 to-purple-600/10 blur-2xl transition-transform duration-500 group-hover:scale-150"></div>

Expand Down Expand Up @@ -187,8 +214,8 @@ export const GetStartedPaths = () => {
{/* Sub-Options */}
<div className="mt-auto space-y-3 pt-2">
<Link
to={useBaseUrl("/docs/quickstart/k8s-proxy/")}
className="group/option flex items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 min-h-[92px] transition-all duration-200 hover:border-purple-300 hover:bg-purple-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-purple-500/50 dark:hover:bg-purple-900/10"
to={useBaseUrl(K8S_PROXY.href)}
className="group/option flex min-h-[92px] items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 transition-all duration-200 hover:border-purple-300 hover:bg-purple-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-purple-500/50 dark:hover:bg-purple-900/10"
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 text-purple-700 transition-colors group-hover/option:bg-purple-100 group-hover/option:text-purple-800 dark:bg-purple-900/30 dark:text-purple-200">
Expand All @@ -209,8 +236,8 @@ export const GetStartedPaths = () => {
</Link>

<Link
to={useBaseUrl("/docs/server/installation/")}
className="group/option flex items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 min-h-[92px] transition-all duration-200 hover:border-purple-300 hover:bg-purple-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-purple-500/50 dark:hover:bg-purple-900/10"
to={useBaseUrl(LOCAL_RECORD.href)}
className="group/option flex min-h-[92px] items-start justify-between gap-3 rounded-xl border border-gray-200 bg-white/50 p-3 transition-all duration-200 hover:border-purple-300 hover:bg-purple-50/50 hover:no-underline dark:border-gray-600 dark:bg-gray-800/50 dark:hover:border-purple-500/50 dark:hover:bg-purple-900/10"
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 text-purple-700 transition-colors group-hover/option:bg-purple-100 group-hover/option:text-purple-800 dark:bg-purple-900/30 dark:text-purple-200">
Expand Down
12 changes: 9 additions & 3 deletions src/components/GlossaryCard.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React from "react";
import Link from "@docusaurus/Link";
import ArrowUpRight from "../../static/img/ArrowUpRight.svg";

// Uses Docusaurus `Link` rather than a bare `<a>` for two reasons: a glossary
// entry pointing at a slug that doesn't exist now fails the build
// (`onBrokenLinks: "throw"`) instead of shipping a 404 that only surfaces in a
// crawl, and `Link` applies the site's `trailingSlash` setting, so these
// hand-written slash-less paths stop costing a 301 on every click.
const GlossaryCard = ({name, description, link}) => {
return (
<a
href={link}
<Link
to={link}
className="group flex h-full flex-col rounded-xl border border-transparent bg-[var(--ifm-card-background-color)] p-6 shadow-[0_4px_12px_var(--ifm-card-shadow-color)] transition-all duration-300 hover:border-[var(--ifm-color-primary)] hover:shadow-[0_8px_20px_var(--ifm-card-shadow-color)] focus:outline-none focus:ring-2 focus:ring-[var(--ifm-color-primary)] focus:ring-offset-2
"
>
Expand All @@ -20,7 +26,7 @@ const GlossaryCard = ({name, description, link}) => {
<p className="text-sm leading-relaxed text-[var(--ifm-color-emphasis-700)]">
{description}
</p>
</a>
</Link>
);
};

Expand Down
Loading
Loading