diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 00000000..afa67f16
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,17 @@
+// Required by Next.js App Router. Every app/ directory MUST have a root layout
+// and it MUST include and
— Next.js 14 will not inject them
+// automatically, and the App Router runtime crashes on root path without them
+// (causing a blank screen on localhost:3000/).
+//
+// All real layout (fonts, global CSS, analytics, structured data) continues to
+// live in pages/_app.tsx and pages/_document.tsx, which serve all pages/ routes
+// unchanged. This layout is only reached for routes inside app/ — currently just
+// app/sitemap.xml/route.ts, which is a Route Handler (not a rendered page), so
+// this layout's is never actually visible to users or crawlers.
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 00000000..bfffffbc
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,23 @@
+"use client";
+import { useEffect } from "react";
+
+// Required when app/ directory is present alongside pages/.
+// Without this file, any unmatched App Router path (e.g. localhost:3000/ outside
+// the basePath) shows a blank "__next_error__" screen instead of a proper page.
+//
+// redirect() from next/navigation cannot be used here because not-found boundaries
+// run after the error has already been thrown. A client-side redirect is the
+// correct pattern for this boundary.
+export default function NotFound() {
+ useEffect(() => {
+ // Redirect to the Pages Router 404 page which has the full blog layout.
+ // basePath /blog is not prepended by window.location — use the full path.
+ window.location.replace("/blog/404");
+ }, []);
+
+ return (
+
+
Page not found — redirecting...
+
+ );
+}
diff --git a/app/sitemap.xml/route.ts b/app/sitemap.xml/route.ts
new file mode 100644
index 00000000..fe129c88
--- /dev/null
+++ b/app/sitemap.xml/route.ts
@@ -0,0 +1,79 @@
+import { getAllPostsForSitemap } from "../../lib/api-server";
+import {
+ adaptPostsForSitemap,
+ assertFullSitemap,
+ buildAuthorEntries,
+ buildPostEntries,
+ buildTagEntries,
+ dedupeEntries,
+ getLatestModified,
+ getStaticFallbackXml,
+ serializeSitemap,
+ STATIC_ROUTES,
+} from "../../lib/sitemap";
+
+// ISR: Vercel caches this response in its CDN edge cache for 1 hour.
+//
+// After first generation: every request is served from CDN (<10ms, no Lambda invoked).
+// After TTL expires: stale version served immediately, regeneration happens in background.
+// If WordPress is down during regen: Vercel keeps serving the previous good version automatically.
+// Cold-start / first request: mitigated by post-deploy warming via .github/workflows/prewarm-sitemap.yml.
+export const revalidate = 3600;
+
+export async function GET(): Promise {
+ try {
+ // getAllPostsForSitemap() uses node:https directly (see lib/api-server.ts) to
+ // bypass Next.js App Router's RSC fetch instrumentation, which causes Cloudflare
+ // to return 502 HTML when global fetch() is used in this context.
+ const allPostsResult = await getAllPostsForSitemap();
+ const posts = adaptPostsForSitemap(allPostsResult);
+
+ // Reject partial WordPress responses — ISR will not cache a thrown error,
+ // so Vercel keeps serving the previous good cached version automatically.
+ assertFullSitemap(posts);
+
+ // Static routes get lastmod = newest post modification time,
+ // so listing pages reflect when the freshest underlying content changed.
+ const latestModified = getLatestModified(posts) ?? new Date().toISOString();
+ const staticEntries = STATIC_ROUTES.map((r) => ({
+ ...r,
+ lastModified: latestModified,
+ }));
+
+ const entries = dedupeEntries([
+ ...staticEntries,
+ ...buildPostEntries(posts),
+ ...buildAuthorEntries(posts),
+ ...buildTagEntries(posts),
+ ]);
+
+ const xml = serializeSitemap(entries);
+
+ return new Response(xml, {
+ status: 200,
+ headers: {
+ "Content-Type": "application/xml",
+ // s-maxage: CDN caches for 1h. stale-while-revalidate: serve stale while
+ // regenerating in background. max-age=0: browsers always revalidate with CDN.
+ "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=3600",
+ },
+ });
+ } catch (error) {
+ console.error(
+ "Sitemap generation failed — serving static-routes-only fallback. " +
+ "Verify WORDPRESS_API_URL is reachable and WPGraphQL is responding.",
+ error
+ );
+
+ // ISR does NOT cache non-2xx responses.
+ // no-store prevents any downstream proxy from caching this degraded response,
+ // so crawlers will retry on the next request once WordPress is back.
+ return new Response(getStaticFallbackXml(), {
+ status: 503,
+ headers: {
+ "Content-Type": "application/xml",
+ "Cache-Control": "no-store",
+ },
+ });
+ }
+}
diff --git a/components/AuthorMapping.tsx b/components/AuthorMapping.tsx
index 352f9b58..280d82fa 100644
--- a/components/AuthorMapping.tsx
+++ b/components/AuthorMapping.tsx
@@ -13,8 +13,8 @@ export default function AuthorMapping({
}) {
const [currentPage, setCurrentPage] = useState(1);
- const authorData = [];
- const ppmaAuthorNameArray = [];
+ const authorData: Array<{ publishingAuthor: string; ppmaAuthorName: string; avatarUrl: string; slug: string }> = [];
+ const ppmaAuthorNameArray: string[] = [];
AuthorArray.forEach((item) => {
const ppmaAuthorName = formatAuthorName(item.ppmaAuthorName);
diff --git a/components/NotFoundPage.tsx b/components/NotFoundPage.tsx
index 2456c468..2e53ec0b 100644
--- a/components/NotFoundPage.tsx
+++ b/components/NotFoundPage.tsx
@@ -219,13 +219,13 @@ const NotFoundPage = ({ latestPosts, communityPosts, technologyPosts }: NotFound
) : (
<>
- {latestPosts?.edges?.length > 0 && (
+ {(latestPosts?.edges?.length ?? 0) > 0 && (
Latest from Our Blog
- {latestPosts.edges.slice(0, 6).map(({ node: post }) => (
+ {(latestPosts?.edges ?? []).slice(0, 6).map(({ node: post }) => (
)}
- {communityPosts?.edges?.length > 0 && (
+ {(communityPosts?.edges?.length ?? 0) > 0 && (
Latest Community Blogs
- {communityPosts.edges.slice(0, 6).map(({ node: post }) => (
+ {(communityPosts?.edges ?? []).slice(0, 6).map(({ node: post }) => (
)}
- {technologyPosts?.edges?.length > 0 && (
+ {(technologyPosts?.edges?.length ?? 0) > 0 && (
Latest Technology Blogs
- {technologyPosts.edges.slice(0, 6).map(({ node: post }) => (
+ {(technologyPosts?.edges ?? []).slice(0, 6).map(({ node: post }) => (
>(null);
+ const timeout = useRef | null>(null);
const wrapperRef = useRef(null);
const handleEnter = () => {
@@ -106,7 +106,7 @@ export default function TOC({ headings, isList, setIsList }) {
const element = document.getElementById(sanitizedId);
if (element) {
window.scrollTo({ top: element.offsetTop - 80, behavior: "smooth" });
- window.history.replaceState(null, null, `#${sanitizedId}`);
+ window.history.replaceState(null, "", `#${sanitizedId}`);
}
};
diff --git a/components/more-stories.tsx b/components/more-stories.tsx
index 93671c73..b1e3a9c6 100644
--- a/components/more-stories.tsx
+++ b/components/more-stories.tsx
@@ -40,7 +40,7 @@ export default function MoreStories({
const [visibleCount, setVisibleCount] = useState(12);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(initialPageInfo?.hasNextPage ?? true);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
const [endCursor, setEndCursor] = useState(initialPageInfo?.endCursor ?? null);
const [buffer, setBuffer] = useState<{ node: Post }[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
diff --git a/components/post-body.tsx b/components/post-body.tsx
index 60890268..fda288ea 100644
--- a/components/post-body.tsx
+++ b/components/post-body.tsx
@@ -47,9 +47,9 @@ export default function PostBody({
slug: string | string[] | undefined;
categories?: Post["categories"];
}) {
- const [tocItems, setTocItems] = useState([]);
- const [copySuccessList, setCopySuccessList] = useState([]);
- const [headingCopySuccessList, setHeadingCopySuccessList] = useState([]);
+ const [tocItems, setTocItems] = useState<{ id: string; title: string | null; type: string }[]>([]);
+ const [copySuccessList, setCopySuccessList] = useState([]);
+ const [headingCopySuccessList, setHeadingCopySuccessList] = useState([]);
const [isSmallScreen, setIsSmallScreen] = useState(false);
const [replacedContent, setReplacedContent] = useState(content || "");
const [isList, setIsList] = useState(false);
diff --git a/lib/api-server.ts b/lib/api-server.ts
new file mode 100644
index 00000000..b13c1a52
--- /dev/null
+++ b/lib/api-server.ts
@@ -0,0 +1,125 @@
+/**
+ * Server-only API helpers.
+ *
+ * This module uses node:https directly instead of the global fetch() because
+ * Next.js App Router's RSC context wraps fetch() with its own instrumentation
+ * layer. That instrumented fetch sends headers that Cloudflare (in front of
+ * wp.keploy.io) rejects with 502 HTML instead of JSON.
+ *
+ * node:https is a raw TCP connection — no Next.js middleware in the path —
+ * identical to what curl or plain `node -e "fetch()"` sends.
+ *
+ * DO NOT import this file from client components. It uses node:https which
+ * does not exist in the browser. All client-side API calls go through
+ * lib/api.ts which uses the standard fetch() API.
+ */
+import https from "node:https";
+
+function getApiUrl(): string {
+ const url = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL;
+ if (!url) {
+ throw new Error(
+ "WordPress API URL is not configured. Set WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL."
+ );
+ }
+ return url;
+}
+
+function fetchGraphQL(query: string, variables: Record = {}): Promise {
+ const apiUrl = getApiUrl();
+ const url = new URL(apiUrl);
+ const body = JSON.stringify({ query, variables });
+
+ return new Promise((resolve, reject) => {
+ const req = https.request(
+ {
+ hostname: url.hostname,
+ port: url.port || 443,
+ path: url.pathname + url.search,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Content-Length": Buffer.byteLength(body),
+ "User-Agent": "keploy-blog-sitemap/1.0",
+ },
+ },
+ (res) => {
+ let data = "";
+ res.on("data", (chunk) => (data += chunk));
+ res.on("end", () => {
+ try {
+ const json = JSON.parse(data);
+ if (json.errors) reject(new Error(JSON.stringify(json.errors)));
+ else resolve(json.data);
+ } catch {
+ reject(
+ new Error(
+ `WordPress returned non-JSON (HTTP ${res.statusCode}): ${data.slice(0, 120)}`
+ )
+ );
+ }
+ });
+ }
+ );
+ req.on("error", reject);
+ req.write(body);
+ req.end();
+ });
+}
+
+/**
+ * Fetches all posts with only the fields needed for sitemap generation.
+ * Uses the same cursor-based pagination as getAllPosts() in lib/api.ts but
+ * requests a smaller field set (no title/excerpt/featuredImage) to reduce
+ * payload size across the ~10 pagination pages.
+ */
+export async function getAllPostsForSitemap(): Promise<{ edges: any[] }> {
+ let allEdges: any[] = [];
+ let hasNextPage = true;
+ let endCursor: string | null = null;
+
+ while (hasNextPage) {
+ const data = await fetchGraphQL(
+ `
+ query AllPostsForSitemap($after: String) {
+ posts(first: 50, after: $after, where: { orderby: { field: DATE, order: DESC } }) {
+ edges {
+ node {
+ slug
+ modified
+ ppmaAuthorName
+ categories {
+ edges {
+ node {
+ name
+ slug
+ }
+ }
+ }
+ tags {
+ edges {
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ `,
+ { after: endCursor }
+ );
+
+ const edges = data?.posts?.edges ?? [];
+ allEdges = [...allEdges, ...edges];
+ hasNextPage = data?.posts?.pageInfo?.hasNextPage ?? false;
+ endCursor = data?.posts?.pageInfo?.endCursor ?? null;
+ }
+
+ return { edges: allEdges };
+}
diff --git a/lib/api.ts b/lib/api.ts
index fd596f50..e69f9a2c 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -1,7 +1,15 @@
export const maxDuration = 300; // This can run Vercel Functions for a maximum of 300 seconds
export const dynamic = 'force-dynamic';
-const API_URL = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL
+const API_URL: string = (() => {
+ const url = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL;
+ if (!url) {
+ throw new Error(
+ "WordPress API URL is not configured. Set WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL in your environment variables."
+ );
+ }
+ return url;
+})();
/**
* Normalize a post node from WordPress — default null title/excerpt to empty
@@ -75,8 +83,8 @@ export async function getPreviewPost(id, idType = "DATABASE_ID") {
export async function getAllTags() {
let hasNextPage = true;
- let endCursor = null;
- let allTags = [];
+ let endCursor: string | null = null;
+ let allTags: any[] = [];
while (hasNextPage) {
const data = await fetchAPI(
@@ -159,9 +167,9 @@ export async function getAllPostsFromTags(tagName: String, preview) {
}
export async function getAllPosts() {
- let allEdges = [];
+ let allEdges: any[] = [];
let hasNextPage = true;
- let endCursor = null;
+ let endCursor: string | null = null;
while (hasNextPage) {
const data = await fetchAPI(
@@ -174,6 +182,7 @@ export async function getAllPosts() {
excerpt
slug
date
+ modified
postId
featuredImage {
node {
@@ -187,6 +196,14 @@ export async function getAllPosts() {
}
ppmaAuthorName
categories {
+ edges {
+ node {
+ name
+ slug
+ }
+ }
+ }
+ tags {
edges {
node {
name
@@ -195,6 +212,10 @@ export async function getAllPosts() {
}
}
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
`,
@@ -406,9 +427,9 @@ export async function getAllPostsForCommunity(preview = false, after = null) {
}
export async function getAllAuthors() {
- let allAuthors = [];
+ let allAuthors: any[] = [];
let hasNextPage = true;
- let endCursor = null;
+ let endCursor: string | null = null;
while (hasNextPage) {
const data = await fetchAPI(
@@ -452,9 +473,9 @@ export async function getAllAuthors() {
}
export async function getPostsByAuthor() {
- let allPosts = [];
+ let allPosts: any[] = [];
let hasNextPage = true;
- let endCursor = null;
+ let endCursor: string | null = null;
while (hasNextPage) {
const data = await fetchAPI(
@@ -504,7 +525,7 @@ export async function getPostsByAuthor() {
export async function getMoreStoriesForSlugs(tags, slug) {
const tagFilter = tags?.edges?.length > 0;
const variables = tagFilter ? { tags: tags.edges.map((edge) => edge.node.name) } : undefined;
- let stories = [];
+ let stories: any[] = [];
let data;
const queryWithTags = `
diff --git a/lib/google-search-console.ts b/lib/google-search-console.ts
new file mode 100644
index 00000000..315555ac
--- /dev/null
+++ b/lib/google-search-console.ts
@@ -0,0 +1,144 @@
+import crypto from "crypto";
+import { SITE_URL } from "./structured-data";
+
+const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
+const GOOGLE_WEBMASTERS_SCOPE = "https://www.googleapis.com/auth/webmasters";
+const GOOGLE_SITEMAPS_SUBMIT_BASE_URL = "https://www.googleapis.com/webmasters/v3/sites";
+const GOOGLE_TOKEN_LIFETIME_SECONDS = 3600;
+const GOOGLE_FETCH_TIMEOUT_MS = 25000;
+
+function getRequiredEnv(name: string) {
+ const value = process.env[name]?.trim();
+ if (!value) {
+ throw new Error(`Missing required environment variable: ${name}`);
+ }
+
+ return value;
+}
+
+function base64UrlEncode(value: Buffer | string) {
+ return Buffer.from(value)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/g, "");
+}
+
+function getGooglePrivateKey() {
+ return getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY").replace(/\\n/g, "\n");
+}
+
+function createServiceAccountJwt() {
+ const clientEmail = getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL");
+ const now = Math.floor(Date.now() / 1000);
+
+ const header = {
+ alg: "RS256",
+ typ: "JWT",
+ };
+
+ const payload = {
+ iss: clientEmail,
+ scope: GOOGLE_WEBMASTERS_SCOPE,
+ aud: GOOGLE_OAUTH_TOKEN_URL,
+ exp: now + GOOGLE_TOKEN_LIFETIME_SECONDS,
+ iat: now,
+ };
+
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
+ const unsignedToken = `${encodedHeader}.${encodedPayload}`;
+
+ const signer = crypto.createSign("RSA-SHA256");
+ signer.update(unsignedToken);
+ signer.end();
+
+ const signature = signer.sign(getGooglePrivateKey());
+
+ return `${unsignedToken}.${base64UrlEncode(signature)}`;
+}
+
+async function fetchGoogleAccessToken() {
+ const assertion = createServiceAccountJwt();
+
+ const response = await fetch(GOOGLE_OAUTH_TOKEN_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams({
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ assertion,
+ }),
+ signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS),
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.text().catch(() => "");
+ throw new Error(
+ `Google OAuth token request failed: ${response.status} ${response.statusText}${
+ errorBody ? ` - ${errorBody}` : ""
+ }`
+ );
+ }
+
+ const json = (await response.json()) as {
+ access_token?: string;
+ };
+
+ if (!json.access_token) {
+ throw new Error("Google OAuth token response did not include an access token");
+ }
+
+ return json.access_token;
+}
+
+function getSearchConsoleSiteUrl() {
+ return getRequiredEnv("GOOGLE_SEARCH_CONSOLE_SITE_URL");
+}
+
+function getSitemapUrl() {
+ return process.env.SITEMAP_PUBLIC_URL?.trim() || `${SITE_URL}/sitemap.xml`;
+}
+
+export function isSearchConsoleSubmissionConfigured() {
+ return Boolean(
+ process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL?.trim() &&
+ process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY?.trim() &&
+ process.env.GOOGLE_SEARCH_CONSOLE_SITE_URL?.trim()
+ );
+}
+
+export async function submitSitemapToSearchConsole() {
+ const accessToken = await fetchGoogleAccessToken();
+ const siteUrl = getSearchConsoleSiteUrl();
+ const sitemapUrl = getSitemapUrl();
+
+ const response = await fetch(
+ `${GOOGLE_SITEMAPS_SUBMIT_BASE_URL}/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(
+ sitemapUrl
+ )}`,
+ {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS),
+ }
+ );
+
+ if (!response.ok) {
+ const errorBody = await response.text().catch(() => "");
+ throw new Error(
+ `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${
+ errorBody ? ` - ${errorBody}` : ""
+ }`
+ );
+ }
+
+ return {
+ siteUrl,
+ sitemapUrl,
+ submittedAt: new Date().toISOString(),
+ };
+}
diff --git a/lib/sitemap.ts b/lib/sitemap.ts
new file mode 100644
index 00000000..b8446729
--- /dev/null
+++ b/lib/sitemap.ts
@@ -0,0 +1,254 @@
+import { SITE_URL } from "./structured-data";
+import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug";
+import { sanitizeStringForURL } from "../utils/sanitizeStringForUrl";
+
+type SitemapChangeFrequency = "daily" | "weekly" | "monthly";
+type CategoryRoute = "technology" | "community";
+
+export type SitemapEntry = {
+ // final absolute url that will appear inside .
+ url: string;
+
+ // final normalized timestamp that will appear inside when available.
+ lastModified?: string;
+
+ // optional sitemap hint for crawlers.
+ changeFrequency?: SitemapChangeFrequency;
+
+ // optional sitemap hint for relative importance.
+ priority?: number;
+};
+
+// internal shape used by all entry builders.
+// populated via adaptPostsForSitemap() from the getAllPosts() return value.
+type SitemapPost = {
+ slug: string;
+ modified?: string;
+ authorName?: string;
+ tags: string[];
+ routes: CategoryRoute[];
+};
+
+export const STATIC_ROUTES: Array> = [
+ // top-level listing and navigation pages that should always be in the sitemap.
+ { url: SITE_URL, changeFrequency: "daily", priority: 1.0 },
+ { url: `${SITE_URL}/technology`, changeFrequency: "daily", priority: 0.9 },
+ { url: `${SITE_URL}/community`, changeFrequency: "daily", priority: 0.9 },
+ { url: `${SITE_URL}/authors`, changeFrequency: "weekly", priority: 0.8 },
+ { url: `${SITE_URL}/tag`, changeFrequency: "weekly", priority: 0.8 },
+ { url: `${SITE_URL}/search`, changeFrequency: "weekly", priority: 0.6 },
+ { url: `${SITE_URL}/community/search`, changeFrequency: "weekly", priority: 0.6 },
+];
+
+// minimum posts required per category before the sitemap is considered trustworthy.
+// prevents a degraded partial wordpress response from replacing a good cached version.
+const MIN_POSTS_PER_CATEGORY = 5;
+
+// maps wordpress category data to the two supported frontend route namespaces.
+// matches by both slug and name (lowercased) to handle editorial inconsistencies.
+function mapCategoriesToRoutes(categories?: {
+ edges?: Array<{ node?: { name?: string; slug?: string } }>;
+}): CategoryRoute[] {
+ const routes = new Set();
+
+ for (const edge of categories?.edges || []) {
+ const slug = edge?.node?.slug?.trim()?.toLowerCase();
+ const name = edge?.node?.name?.trim()?.toLowerCase();
+
+ if (slug === "technology" || name === "technology") routes.add("technology");
+ if (slug === "community" || name === "community") routes.add("community");
+ }
+
+ return Array.from(routes);
+}
+
+// converts the getAllPosts() return shape into SitemapPost[].
+// this is the only coupling point between lib/api.ts and lib/sitemap.ts.
+// posts with no matching category route are excluded — same rule as before.
+export function adaptPostsForSitemap(allPostsResult: {
+ edges: Array<{ node: any }>;
+}): SitemapPost[] {
+ const posts: SitemapPost[] = [];
+
+ for (const edge of allPostsResult?.edges || []) {
+ const node = edge?.node;
+ if (!node?.slug) continue;
+
+ const routes = mapCategoriesToRoutes(node.categories);
+ if (!routes.length) continue;
+
+ posts.push({
+ slug: node.slug,
+ modified: node.modified,
+ authorName: node.ppmaAuthorName,
+ routes,
+ tags:
+ node.tags?.edges
+ ?.map((e: any) => e?.node?.name?.trim())
+ .filter((n: unknown): n is string => Boolean(n)) || [],
+ });
+ }
+
+ return posts;
+}
+
+// throws if the crawl looks incomplete, preventing partial data from being cached.
+export function assertFullSitemap(posts: SitemapPost[]) {
+ if (!posts.length) {
+ throw new Error("Sitemap generation returned zero posts");
+ }
+
+ const technologyCount = posts.filter((p) => p.routes.includes("technology")).length;
+ const communityCount = posts.filter((p) => p.routes.includes("community")).length;
+
+ if (technologyCount < MIN_POSTS_PER_CATEGORY || communityCount < MIN_POSTS_PER_CATEGORY) {
+ throw new Error(
+ `Sitemap generation incomplete: technology=${technologyCount}, community=${communityCount} (minimum ${MIN_POSTS_PER_CATEGORY} required per category)`
+ );
+ }
+}
+
+function toIsoDate(value?: string) {
+ if (!value) return undefined;
+ const parsed = new Date(value);
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
+}
+
+function isRecent(dateValue?: string, days = 30) {
+ const isoDate = toIsoDate(dateValue);
+ if (!isoDate) return false;
+ const cutoff = new Date();
+ cutoff.setDate(cutoff.getDate() - days);
+ return new Date(isoDate) >= cutoff;
+}
+
+// finds the newest modified timestamp across all included posts.
+// used to set lastmod on static listing pages so they reflect freshest content.
+export function getLatestModified(posts: SitemapPost[]) {
+ return posts.reduce((latest, post) => {
+ const current = toIsoDate(post.modified);
+ if (!current) return latest;
+ if (!latest || current > latest) return current;
+ return latest;
+ }, undefined);
+}
+
+// one entry per post per matching route.
+// a post in both technology and community produces two urls.
+export function buildPostEntries(posts: SitemapPost[]): SitemapEntry[] {
+ return posts.flatMap((post) =>
+ post.routes.map((route) => ({
+ url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`,
+ lastModified: toIsoDate(post.modified),
+ changeFrequency: "weekly" as const,
+ // posts modified in the last 30 days get higher priority
+ priority: isRecent(post.modified) ? 0.8 : 0.5,
+ }))
+ );
+}
+
+// one entry per unique author derived from included posts.
+// lastmod is the newest modification time of any post by that author.
+export function buildAuthorEntries(posts: SitemapPost[]): SitemapEntry[] {
+ const authorMap = new Map();
+
+ for (const post of posts) {
+ const authorName = post.authorName?.trim();
+ if (!authorName) continue;
+
+ const authorSlug = sanitizeAuthorSlug(authorName);
+ if (!authorSlug) continue;
+
+ const currentModified = toIsoDate(post.modified);
+ const existingModified = authorMap.get(authorSlug);
+
+ if (!existingModified || (currentModified && currentModified > existingModified)) {
+ authorMap.set(authorSlug, currentModified);
+ }
+ }
+
+ return Array.from(authorMap.entries()).map(([authorSlug, lastModified]) => ({
+ url: `${SITE_URL}/authors/${authorSlug}`,
+ lastModified,
+ changeFrequency: "weekly" as const,
+ priority: 0.7,
+ }));
+}
+
+// one entry per unique tag derived from included posts.
+// lastmod is the newest modification time of any post with that tag.
+export function buildTagEntries(posts: SitemapPost[]): SitemapEntry[] {
+ const tagMap = new Map();
+
+ for (const post of posts) {
+ const postModified = toIsoDate(post.modified);
+
+ for (const tagName of post.tags) {
+ const normalizedTag = tagName.trim();
+ if (!normalizedTag) continue;
+
+ const existingModified = tagMap.get(normalizedTag);
+ if (!existingModified || (postModified && postModified > existingModified)) {
+ tagMap.set(normalizedTag, postModified);
+ }
+ }
+ }
+
+ return Array.from(tagMap.entries()).flatMap(([tagName, lastModified]) => {
+ const tagSlug = sanitizeStringForURL(tagName);
+ if (!tagSlug) return [];
+ return [{
+ url: `${SITE_URL}/tag/${tagSlug}`,
+ lastModified,
+ changeFrequency: "weekly" as const,
+ priority: 0.7,
+ }];
+ });
+}
+
+// keeps one entry per url — last writer wins if two paths produce the same url.
+export function dedupeEntries(entries: SitemapEntry[]): SitemapEntry[] {
+ return Array.from(new Map(entries.map((e) => [e.url, e])).values());
+}
+
+// serializes sitemap entries to xml manually — no external library.
+// all values are escaped. priority is formatted to one decimal place.
+export function serializeSitemap(entries: SitemapEntry[]): string {
+ const body = entries
+ .map((entry) => {
+ const parts = [
+ `${escapeXml(entry.url)}`,
+ entry.lastModified ? `${escapeXml(entry.lastModified)}` : "",
+ entry.changeFrequency ? `${entry.changeFrequency}` : "",
+ typeof entry.priority === "number"
+ ? `${entry.priority.toFixed(1)}`
+ : "",
+ ].filter(Boolean);
+
+ return `${parts.join("")}`;
+ })
+ .join("");
+
+ return (
+ `` +
+ `${body}`
+ );
+}
+
+function escapeXml(value: string): string {
+ return value
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+}
+
+// last-resort xml served when generation fails with no cached version available.
+// contains only the 7 hardcoded static routes — no wordpress data required.
+export function getStaticFallbackXml(): string {
+ const now = new Date().toISOString();
+ return serializeSitemap(
+ STATIC_ROUTES.map((route) => ({ ...route, lastModified: now }))
+ );
+}
diff --git a/next.config.js b/next.config.js
index 38bdc6b9..ccb795a8 100644
--- a/next.config.js
+++ b/next.config.js
@@ -53,7 +53,9 @@ module.exports = {
async headers() {
return [
{
- source: '/(.*)',
+ // exclude sitemap.xml — xml documents do not use csp and vercel.json
+ // also excludes it, so keep both layers consistent.
+ source: '/((?!sitemap\\.xml$).*)',
headers: [
{
key: 'Content-Security-Policy',
diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts
new file mode 100644
index 00000000..bf0b192e
--- /dev/null
+++ b/pages/api/cron/refresh-sitemap.ts
@@ -0,0 +1,62 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import {
+ isSearchConsoleSubmissionConfigured,
+ submitSitemapToSearchConsole,
+} from "../../../lib/google-search-console";
+
+// GSC submission is fast — no WordPress crawl happens here anymore.
+// Sitemap generation is handled by ISR in app/sitemap.xml/route.ts.
+export const config = { maxDuration: 30 };
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const expectedSecret = process.env.CRON_SECRET;
+
+ // distinguish a deployment misconfiguration (500) from a wrong token (401).
+ if (!expectedSecret) {
+ console.error(
+ "CRON_SECRET is not configured. Set it in Vercel environment variables and redeploy."
+ );
+ return res.status(500).json({
+ ok: false,
+ message: "Server misconfiguration — CRON_SECRET is not configured",
+ });
+ }
+
+ // auth is checked before method to avoid leaking valid HTTP methods to
+ // unauthenticated callers. vercel cron automatically injects this header.
+ if (req.headers.authorization !== `Bearer ${expectedSecret}`) {
+ return res.status(401).json({ ok: false, message: "Unauthorized" });
+ }
+
+ if (req.method !== "GET") {
+ res.setHeader("Allow", "GET");
+ return res.status(405).json({ ok: false, message: "Method not allowed" });
+ }
+
+ // skip silently if google search console env vars are not all configured.
+ if (!isSearchConsoleSubmissionConfigured()) {
+ return res.status(200).json({
+ ok: true,
+ message: "Google Search Console submission is not configured — skipped",
+ });
+ }
+
+ try {
+ // notify google that the sitemap has been updated so it re-crawls it.
+ // the sitemap itself is generated and cached by ISR — no crawl needed here.
+ const result = await submitSitemapToSearchConsole();
+ return res.status(200).json({ ok: true, ...result });
+ } catch (error) {
+ console.error(
+ "Google Search Console sitemap submission failed. " +
+ "Verify GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY, " +
+ "GOOGLE_SEARCH_CONSOLE_SITE_URL, and Search Console property access for the service account.",
+ error
+ );
+ return res.status(500).json({
+ ok: false,
+ message:
+ error instanceof Error ? error.message : "Google Search Console submission failed",
+ });
+ }
+}
diff --git a/pages/authors/[slug].tsx b/pages/authors/[slug].tsx
index 67609d52..8f39a232 100644
--- a/pages/authors/[slug].tsx
+++ b/pages/authors/[slug].tsx
@@ -111,7 +111,7 @@ export const getStaticProps: GetStaticProps = async ({
candidateAuthorNames.add(slugWords[0]);
}
- let filteredPosts = [];
+ let filteredPosts: any[] = [];
for (const candidate of Array.from(candidateAuthorNames)) {
if (!candidate) continue;
diff --git a/pages/community/[slug].tsx b/pages/community/[slug].tsx
index c83d6534..e139d8f9 100644
--- a/pages/community/[slug].tsx
+++ b/pages/community/[slug].tsx
@@ -85,7 +85,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
},
];
- const postBodyRef = useRef();
+ const postBodyRef = useRef(null);
const readProgress = useSpringValue(0);
useScroll({
onChange(v) {
@@ -148,7 +148,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
const safeDescription = getSafeDescription(router.isFallback, post?.seo?.metaDesc, safeTitle);
const postUrl = post?.slug ? `${SITE_URL}/community/${post.slug}` : `${SITE_URL}/community`;
- const structuredData = [];
+ const structuredData: any[] = [];
if (post?.slug) {
structuredData.push(
getBreadcrumbListSchema([
diff --git a/pages/technology/[slug].tsx b/pages/technology/[slug].tsx
index 44413bb4..ffd20a69 100644
--- a/pages/technology/[slug].tsx
+++ b/pages/technology/[slug].tsx
@@ -81,7 +81,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
description: reviewAuthorDescription || "A Reviewer for keploy's blog",
},
];
- const postBodyRef = useRef();
+ const postBodyRef = useRef(null);
const readProgress = useSpringValue(0);
useScroll({
onChange(v) {
@@ -138,7 +138,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
const safeDescription = getSafeDescription(router.isFallback, post?.seo?.metaDesc, safeTitle);
const postUrl = post?.slug ? `${SITE_URL}/technology/${post.slug}` : `${SITE_URL}/technology`;
- const structuredData = [];
+ const structuredData: any[] = [];
if (post?.slug) {
structuredData.push(
getBreadcrumbListSchema([
diff --git a/playwright.config.ts b/playwright.config.ts
index 237f3b15..984b33fc 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -116,6 +116,7 @@ export default defineConfig({
env: {
WORDPRESS_API_URL: GRAPHQL_API_URL,
NEXT_PUBLIC_WORDPRESS_API_URL: GRAPHQL_API_URL,
+ CRON_SECRET: 'test-secret',
},
},
],
diff --git a/public/sitemap.xml b/public/sitemap.xml
deleted file mode 100644
index 45fa21f6..00000000
--- a/public/sitemap.xml
+++ /dev/null
@@ -1,1571 +0,0 @@
-
-
-
-
-
- https://keploy.io/blog
- 2024-03-07T09:25:36+00:00
- 1.00
-
-
- https://keploy.io/blog/technology
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/community
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/technology/mongodb-in-mock-mode-acting-the-server-part
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/technology/capture-grpc-traffic-going-out-from-a-server
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/technology/integration-vs-e2e-testing-what-worked-for-me-as-a-charm
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/community/canary-testing-a-comprehensive-guide-for-developers
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/community/mock-vs-stub-vs-fake-understand-the-difference
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/community/writing-test-cases-for-cron-jobs-testing
- 2024-03-07T09:25:36+00:00
- 0.80
-
-
- https://keploy.io/blog/technology/automated-e2e-tests-using-property-based-testing-part-ii
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/technology/automated-end-to-end-tests-using-property-based-testing-part-i
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/technology/go-mocks-and-stubs-made-easy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understand-the-role-of-continuous-testing-in-ci-cd
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-testing-in-production
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/5-unit-testing-tools-you-must-know-in-2024
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/securing-data-protocols-tls-application
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/demystifying-cron-job-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/building-custom-yaml-dsl-in-python
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-is-service-mesh
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-condition-coverage-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/why-do-i-need-a-unit-testing-tool
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/revolutionizing-software-testing-with-feature-flags
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/all-about-system-integration-testing-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/bdd-testing-with-cucumber
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-choose-your-api-performance-testing-tool-a-guide
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/dignify-your-test-automation-with-concise-code-documentation
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-do-java-unit-testing-effectively
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/performance-testing-guide-to-ensure-your-software-performs-at-its-best
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/top-5-cypress-alternatives-for-web-testing-and-automation
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-is-quality-engineering-software
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/functional-testing-unveiling-types-and-real-world-applications
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-branch-coverage-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/creating-the-balance-between-end-to-end-and-unit-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-code-coverage-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/decoding-http2-traffic-is-hard-but-ebpf-can-help
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/testng-vs-junit-performance-ease-of-use-and-flexibility-compared
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/exploring-the-effectiveness-of-e2e-testing-in-comparison-with-integration-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-statement-coverage-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/why-i-love-end-to-end-e2e-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-generate-test-cases-with-automation-tools
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/decoding-brd-a-devs-guide-to-functional-and-non-functional-requirements-in-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/testing-in-production-with-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/mastering-test-coverage-quality-over-quantity-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/all-about-api-testing-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/exploring-end-to-end-testing-with-ai
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/ai-powered-testing-in-production-revolutionizing-software-stability
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-is-the-difference-between-uat-and-e2e-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/software-development-phases
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/testing-nirvana-unveiled-what-why-and-how-in-development
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-problem-keploy-solves
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/getting-started-with-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/mastering-api-test-automation-best-practices-and-tools
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/why-more-end-to-end-testing-is-often-good-enough-for-less-stress
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/e2e-testing-strategies-handling-edge-cases-while-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-the-difference-between-test-scenarios-and-test-cases
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/qa-automation-engineers-overcoming-testing-limitations
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/code-integrity-explained-building-trust-in-software
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/testing-with-chatgpt-epic-wins-and-fails
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/a-guide-for-observing-go-process-with-ebpf
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/stubs-mocks-fakes-lets-define-the-boundaries
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/e2e-testing-or-unit-testing-difference
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/4-ways-to-accelerate-your-software-testing-life-cycle
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/using-ebpf-for-tracing-go-function-arguments-in-production
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/my-journey-of-devrel-cohort-at-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/building-a-crud-application-from-scratch-using-golang
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/exploring-graphql-api-development
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/diverse-test-data-boosting-regression-testing-efficiency
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/writing-a-potions-bank-rest-api-with-spring-boot-mongodb
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/my-journey-of-automating-test-cases
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/a-guide-to-various-api-architectures
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/postman-features-that-will-help-you-on-your-journey
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/api-automation-testing-pynt-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/fun-facts-about-apis
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/know-about-record-and-replay-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/the-game-of-shadow-testing-the-core-of-test-generation
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/apis-vs-webhooks-make-a-github-webhook
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-secure-your-apis-and-protect-sensitive-data
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/soap-vs-rest-choosing-the-right-api-protocol
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/simplifying-junit-test-stubs-and-mocking
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/terminologies-around-api
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/my-keploy-api-fellowship-journey-2
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-is-unit-testing-anyways
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/what-is-end-to-end-testing-and-why-do-you-need-it
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/an-introduction-to-api-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/everything-you-need-to-know-about-unit-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/my-journey-of-keploy-fellowship-program
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-mock-backend-of-selenium-tests-using-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-to-do-frontend-test-automation-using-selenium
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/types-of-apis-and-api-architecture
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/introduction-to-testing-with-mocha-keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/my-keploy-api-fellowship-journey
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/teleport-into-tech-space-through-api-gateways
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/frustrations-of-api-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/difficulties-of-api-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/swagger-design-and-document-your-apis
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/history-of-apis
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-http-and-https-as-a-beginner
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/understanding-the-components-of-apis
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/devrel-at-keploy-experience
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/community/how-did-i-get-to-know-about-apis
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/Ritik%20Jain
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/go
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/http
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/mongodb
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/proxy-server
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/Mehfooz
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/golang
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/grpc
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/Sarthak%20Shyngle
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/api-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/automation-testing
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/e2e
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/integration-test
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/testgpt
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/Animesh%20Pathak
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/canary
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/Feature%20Flags
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/Arindam
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/ai-tools
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/mocks
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/stub
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/ai%20tool
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/cron-job
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/cronjobs
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/tag/keploy
- 2024-03-07T09:25:36+00:00
- 0.64
-
-
- https://keploy.io/blog/authors/charan
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/apis
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/chatgpt
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/openai
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-automation
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/tdd
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Jain
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/gomock
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-generator
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/unit-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Prajwal
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ci-cd
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/cicd
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/continuous-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/devops
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-in-production
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-coverage
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/unit%20testing%20tool
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Shivam
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/https
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/networking
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/protocols
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/redis
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/tls
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/cronjob
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/code
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/python
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/yaml-dsl
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/community/ebpf-service-mesh-and-sidecar
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ebpf
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/service%20mesh
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/sidecar
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/condition%20coverage
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-coverage-in-software-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/tvisha
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/junit
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/system%20integration
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/bdd%20test
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/cucumber%20js
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/performance
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-tool
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/automation
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/documentation
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/java
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/java%20unit%20testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/performance%20testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/cypress%20alternative
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/katalon
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/developers
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/engineering
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/learning
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/quality-assurance
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-engineering
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/bdd
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/cross-browser-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ecommerce
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/functional-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/security
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/branchcoverage
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/e2e%20testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/end%20to%20end%20test
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/coding
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/deployment
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/http2
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/wireshark
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Pranshu%20Srivastava
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/maven
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-library
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Shashwat
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/end-to-end-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/integration-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-development-life-cyclesdlc
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/backend-developments
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/statement-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/community/end-to-end-testing-and-why-do-you-need-it
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/banking
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/developers-mindset
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/gps
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/jest
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/postman
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/business-requirement-document
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/functional-requirement
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/functional-vs-non-functional-requirements
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/non-functional-requirements
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/prashant
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ai
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ai-based-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-coverage-in-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Arindam,%20Neha
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/web-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/wemakedevs
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/developer-tools
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Aditya%20Tomar
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/e2etesting
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/uat-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Harshit%20Paneri
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-development-phases
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/community/strategies-handling-edge-cases-e2e-tests
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/continuous-deployment
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/continuous-integration
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software-quality-assurance
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-nirvana
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-automation
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/best-practices
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/mocking
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Shashwat%20Gupta
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/edge-cases
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-driven-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-tools
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/end-to-end-tests
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/qa-automation-engineers
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-scenarios
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/testing-limitations
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/code-review
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/software
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Neha%20Gupta
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/generative-ai
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/observability
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/opensource
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/fakes
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/stubs
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-doubles
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Shivang%20Shandilya
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/docker
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/docker-compose
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/postgresql
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/rest-api
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/graphql
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ai-test-generation
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/data-generator
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/regression-test-suite
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/regression-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/test-data-management
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/springboot
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Nishant%20Mishra
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Aditya%20Singh
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-architecture
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-basics
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Sejal%20Jain
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Ankit%20Kumar
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Hardik%20kumar
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/databases
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/github
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/webhooks
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Jyotirmoy%20Roy
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/soap-api
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Sanskriti%20Harmukh
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/mockito
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Barkatul%20Mujauddin
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Yash%20Saxena
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Zoheb%20Ahmed
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/methodology-and-types-of-software-testing
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Priya%20Srivastava
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Diganta%20Kr%20Banik
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-testing-tools
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Krupesh%20Vithlani
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/app-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/developer
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/technology
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Ankit%20Kumar,%20Animesh%20Pathak
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/backend
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/frontend-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/selenium
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/KANISHAK%20CHAURASIA
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/keploy-api-fellowship
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Pradhyuman%20Sharma
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/javascript
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/authors/Harsh%20Rastogi
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/devrel
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-gateway
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/microservices
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/monoliths
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/osi
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/api-development
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/frustration
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/swagger
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/history
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/ssl
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/internship
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/startup
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/technical-writing-1
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
- https://keploy.io/blog/tag/beginners
- 2024-03-07T09:25:36+00:00
- 0.51
-
-
-
\ No newline at end of file
diff --git a/scripts/submit-sitemap-to-search-console.mjs b/scripts/submit-sitemap-to-search-console.mjs
new file mode 100644
index 00000000..24bb2ca1
--- /dev/null
+++ b/scripts/submit-sitemap-to-search-console.mjs
@@ -0,0 +1,160 @@
+import crypto from "crypto";
+import dotenv from "dotenv";
+
+// load .env.local explicitly so this standalone script behaves like the local app
+// and can reuse the same credentials without extra shell setup.
+dotenv.config({ path: ".env.local" });
+
+const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
+const GOOGLE_WEBMASTERS_SCOPE = "https://www.googleapis.com/auth/webmasters";
+const GOOGLE_SITEMAPS_SUBMIT_BASE_URL = "https://www.googleapis.com/webmasters/v3/sites";
+const GOOGLE_TOKEN_LIFETIME_SECONDS = 3600;
+const GOOGLE_FETCH_TIMEOUT_MS = 25000;
+const DEFAULT_SITEMAP_URL = "https://keploy.io/blog/sitemap.xml";
+
+function getRequiredEnv(name) {
+ // fail early if any required local env var is missing.
+ const value = process.env[name]?.trim();
+
+ if (!value) {
+ throw new Error(`Missing required environment variable: ${name}`);
+ }
+
+ return value;
+}
+
+function base64UrlEncode(value) {
+ // convert to jwt-safe base64url form.
+ return Buffer.from(value)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/g, "");
+}
+
+function getGooglePrivateKey() {
+ // restore newline characters so the pem key is valid for crypto signing.
+ return getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY").replace(/\\n/g, "\n");
+}
+
+function createServiceAccountJwt() {
+ const clientEmail = getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL");
+ const now = Math.floor(Date.now() / 1000);
+
+ // construct the service-account jwt the same way the app code does so this
+ // script is a faithful local verification path.
+ const header = {
+ alg: "RS256",
+ typ: "JWT",
+ };
+
+ const payload = {
+ iss: clientEmail,
+ scope: GOOGLE_WEBMASTERS_SCOPE,
+ aud: GOOGLE_OAUTH_TOKEN_URL,
+ exp: now + GOOGLE_TOKEN_LIFETIME_SECONDS,
+ iat: now,
+ };
+
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
+ const unsignedToken = `${encodedHeader}.${encodedPayload}`;
+
+ const signer = crypto.createSign("RSA-SHA256");
+ signer.update(unsignedToken);
+ signer.end();
+
+ const signature = signer.sign(getGooglePrivateKey());
+
+ return `${unsignedToken}.${base64UrlEncode(signature)}`;
+}
+
+async function fetchGoogleAccessToken() {
+ // exchange the signed jwt for a short-lived oauth token.
+ const assertion = createServiceAccountJwt();
+
+ const response = await fetch(GOOGLE_OAUTH_TOKEN_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams({
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ assertion,
+ }),
+ signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS),
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.text().catch(() => "");
+ throw new Error(
+ `Google OAuth token request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""
+ }`
+ );
+ }
+
+ const json = await response.json();
+
+ if (!json.access_token) {
+ throw new Error("Google OAuth token response did not include an access token");
+ }
+
+ return json.access_token;
+}
+
+async function submitSitemapToSearchConsole() {
+ // get the token, the property id, and the sitemap url from local env.
+ const accessToken = await fetchGoogleAccessToken();
+ const siteUrl = getRequiredEnv("GOOGLE_SEARCH_CONSOLE_SITE_URL");
+ const sitemapUrl = process.env.SITEMAP_PUBLIC_URL?.trim() || DEFAULT_SITEMAP_URL;
+
+ // submit the sitemap url directly to google search console so we can verify
+ // the credentials and property access without going through the cron route.
+ const response = await fetch(
+ `${GOOGLE_SITEMAPS_SUBMIT_BASE_URL}/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(
+ sitemapUrl
+ )}`,
+ {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS),
+ }
+ );
+
+ if (!response.ok) {
+ const errorBody = await response.text().catch(() => "");
+ throw new Error(
+ `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""
+ }`
+ );
+ }
+
+ return {
+ ok: true,
+ siteUrl,
+ sitemapUrl,
+ submittedAt: new Date().toISOString(),
+ };
+}
+
+try {
+ // print a compact success object so the local test result is easy to inspect.
+ const result = await submitSitemapToSearchConsole();
+ console.log(JSON.stringify(result, null, 2));
+} catch (error) {
+ // print a compact structured failure object for quick debugging.
+ console.error(
+ JSON.stringify(
+ {
+ ok: false,
+ message: error instanceof Error ? error.message : String(error),
+ },
+ null,
+ 2
+ )
+ );
+ process.exit(1);
+}
+
diff --git a/tests/e2e/RefreshSitemapCron.spec.ts b/tests/e2e/RefreshSitemapCron.spec.ts
new file mode 100644
index 00000000..54a9d6de
--- /dev/null
+++ b/tests/e2e/RefreshSitemapCron.spec.ts
@@ -0,0 +1,51 @@
+import { expect, test } from '@playwright/test';
+
+test.describe('Refresh Sitemap Cron API', () => {
+ test('returns 401 when authorization header is missing', async ({ request, baseURL }) => {
+ const response = await request.get(`${baseURL}/api/cron/refresh-sitemap`);
+ expect(response.status()).toBe(401);
+
+ const body = await response.json();
+ expect(body.ok).toBe(false);
+ });
+
+ test('returns 401 when authorization header is invalid', async ({ request, baseURL }) => {
+ const response = await request.get(`${baseURL}/api/cron/refresh-sitemap`, {
+ headers: {
+ Authorization: 'Bearer wrong-secret',
+ },
+ });
+ expect(response.status()).toBe(401);
+
+ const body = await response.json();
+ expect(body.ok).toBe(false);
+ });
+
+ test('returns 405 for non-GET methods with valid authorization', async ({ request, baseURL }) => {
+ const response = await request.post(`${baseURL}/api/cron/refresh-sitemap`, {
+ headers: {
+ Authorization: 'Bearer test-secret',
+ },
+ });
+ expect(response.status()).toBe(405);
+ expect(response.headers()['allow']).toBe('GET');
+
+ const body = await response.json();
+ expect(body.ok).toBe(false);
+ });
+
+ test('returns 200 and refresh metadata for authorized GET', async ({ request, baseURL }) => {
+ const response = await request.get(`${baseURL}/api/cron/refresh-sitemap`, {
+ headers: {
+ Authorization: 'Bearer test-secret',
+ },
+ });
+ expect(response.status()).toBe(200);
+
+ const body = await response.json();
+ expect(body.ok).toBe(true);
+ expect(body.entryCount).toBeGreaterThan(0);
+ expect(typeof body.generatedAt).toBe('string');
+ expect(typeof body.searchConsole?.submitted).toBe('boolean');
+ });
+});
diff --git a/tests/e2e/Sitemap.spec.ts b/tests/e2e/Sitemap.spec.ts
new file mode 100644
index 00000000..7757a56c
--- /dev/null
+++ b/tests/e2e/Sitemap.spec.ts
@@ -0,0 +1,20 @@
+import { expect, test } from '@playwright/test';
+
+test.describe('Sitemap Route', () => {
+ test('/sitemap.xml should return XML with sitemap core routes', async ({ request, baseURL }) => {
+ const response = await request.get(`${baseURL}/sitemap.xml`);
+
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toContain('application/xml');
+ expect(response.headers()['cache-control']).toContain('s-maxage=86400');
+ expect(response.headers()['cache-control']).toContain('max-age=0');
+ expect(response.headers()['cache-control']).toContain('stale-while-revalidate=86400');
+
+ const xml = await response.text();
+ expect(xml).toContain('');
+ expect(xml).toContain('');
+ expect(xml).toContain('https://keploy.io/blog');
+ expect(xml).toContain('https://keploy.io/blog/technology');
+ expect(xml).toContain('https://keploy.io/blog/community');
+ });
+});
diff --git a/tests/fixtures/community-posts.json b/tests/fixtures/community-posts.json
index e1c01950..945d5dbc 100644
--- a/tests/fixtures/community-posts.json
+++ b/tests/fixtures/community-posts.json
@@ -118,6 +118,90 @@
"title": "From Discord to Docs: Community-Led Quality Improvements | Keploy Blog"
}
}
+ },
+ {
+ "node": {
+ "title": "Open Source Contributions That Improved Keploy Testing",
+ "excerpt": "A look at community pull requests that made Keploy test generation more reliable.
",
+ "slug": "open-source-contributions-keploy-testing",
+ "date": "2024-05-28T10:00:00",
+ "modified": "2024-05-28T10:00:00",
+ "postId": 3004,
+ "featuredImage": {
+ "node": {
+ "sourceUrl": "/blog/favicon/Group.png"
+ }
+ },
+ "author": {
+ "node": {
+ "name": "Community Author Three",
+ "firstName": "Community",
+ "lastName": "Author Three",
+ "avatar": {
+ "url": "/blog/favicon/Group.png"
+ }
+ }
+ },
+ "ppmaAuthorName": "Community Author Three",
+ "categories": {
+ "edges": [
+ {
+ "node": {
+ "name": "community"
+ }
+ }
+ ]
+ },
+ "tags": {
+ "edges": []
+ },
+ "seo": {
+ "metaDesc": "Community pull requests that made Keploy test generation more reliable.",
+ "title": "Open Source Contributions That Improved Keploy Testing | Keploy Blog"
+ }
+ }
+ },
+ {
+ "node": {
+ "title": "Writing Your First Keploy Test: A Community Guide",
+ "excerpt": "Step-by-step guidance from community members on getting started with Keploy test recording.
",
+ "slug": "writing-first-keploy-test-community-guide",
+ "date": "2024-05-15T09:00:00",
+ "modified": "2024-05-15T09:00:00",
+ "postId": 3005,
+ "featuredImage": {
+ "node": {
+ "sourceUrl": "/blog/favicon/Group.png"
+ }
+ },
+ "author": {
+ "node": {
+ "name": "Community Author Four",
+ "firstName": "Community",
+ "lastName": "Author Four",
+ "avatar": {
+ "url": "/blog/favicon/Group.png"
+ }
+ }
+ },
+ "ppmaAuthorName": "Community Author Four",
+ "categories": {
+ "edges": [
+ {
+ "node": {
+ "name": "community"
+ }
+ }
+ ]
+ },
+ "tags": {
+ "edges": []
+ },
+ "seo": {
+ "metaDesc": "Step-by-step guidance on getting started with Keploy test recording.",
+ "title": "Writing Your First Keploy Test: A Community Guide | Keploy Blog"
+ }
+ }
}
],
"pageInfo": {
diff --git a/tests/fixtures/technology-posts.json b/tests/fixtures/technology-posts.json
index 7c8234ef..0500b00b 100644
--- a/tests/fixtures/technology-posts.json
+++ b/tests/fixtures/technology-posts.json
@@ -119,6 +119,48 @@
}
}
},
+ {
+ "node": {
+ "title": "Mocking vs Stubbing in API Tests",
+ "excerpt": "A practical look at when to mock and when to stub in API testing pipelines.
",
+ "slug": "mocking-vs-stubbing-api-tests",
+ "date": "2024-05-20T08:00:00",
+ "modified": "2024-05-20T08:00:00",
+ "postId": 2005,
+ "featuredImage": {
+ "node": {
+ "sourceUrl": "/blog/favicon/Group.png"
+ }
+ },
+ "author": {
+ "node": {
+ "name": "Tech Author Three",
+ "firstName": "Tech",
+ "lastName": "Author Three",
+ "avatar": {
+ "url": "/blog/favicon/Group.png"
+ }
+ }
+ },
+ "ppmaAuthorName": "Tech Author Three",
+ "categories": {
+ "edges": [
+ {
+ "node": {
+ "name": "technology"
+ }
+ }
+ ]
+ },
+ "tags": {
+ "edges": []
+ },
+ "seo": {
+ "metaDesc": "A practical look at when to mock and when to stub in API testing.",
+ "title": "Mocking vs Stubbing in API Tests | Keploy Blog"
+ }
+ }
+ },
{
"node": {
"title": null,
diff --git a/tests/mock-server.js b/tests/mock-server.js
index 11fbb67b..93926a00 100644
--- a/tests/mock-server.js
+++ b/tests/mock-server.js
@@ -14,6 +14,20 @@ const technologyPosts = loadFixture('technology-posts.json');
const communityPosts = loadFixture('community-posts.json');
const singlePost = loadFixture('single-post.json');
const singleCommunityPost = loadFixture('single-community-post.json');
+const sitemapPostsResponse = {
+ data: {
+ posts: {
+ edges: [
+ ...(technologyPosts?.data?.posts?.edges || []),
+ ...(communityPosts?.data?.posts?.edges || []),
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ },
+ },
+ },
+};
const communitySlugs = new Set(
communityPosts.data.posts.edges.map(e => e.node.slug)
@@ -209,6 +223,10 @@ function handleGraphQL(body) {
return technologyPosts;
}
+ if (query.includes('query SitemapPosts')) {
+ return sitemapPostsResponse;
+ }
+
if (query.includes('categoryName: "technology"')) {
return technologyPosts;
}
diff --git a/tsconfig.json b/tsconfig.json
index 1e626d7a..a2983b39 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@@ -16,9 +20,24 @@
"incremental": true,
"baseUrl": ".",
"paths": {
- "@/*": ["./*"]
- }
+ "@/*": [
+ "./*"
+ ]
+ },
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "strictNullChecks": true
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}
diff --git a/vercel.json b/vercel.json
index ab5cb8be..359f525e 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,10 +1,21 @@
{
+ "crons": [
+ {
+ "path": "/blog/api/cron/refresh-sitemap",
+ "schedule": "0 0 * * *"
+ }
+ ],
"redirects": [
{
"source": "/blog/community/everything-you-need-to-know-about-api-testing",
"destination": "/blog/community/what-is-api-testing",
"permanent": true
},
+ {
+ "source": "/blog/community/end-to-end-testing-and-why-do-you-need-it",
+ "destination": "/blog/community/end-to-end-testing-guide",
+ "permanent": true
+ },
{
"source": "/blog/community/regression-testing-tools-rankings-2025",
"destination": "/blog/community/regression-testing-tools",
@@ -41,7 +52,7 @@
]
},
{
- "source": "/blog/(.*)",
+ "source": "/blog/((?!(?:sitemap\\.xml$|api/|_next/static/)).*)",
"headers": [
{
"key": "Content-Security-Policy",