From ecd8aac3fe68ae2ca47d8a310db867da3b610f64 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Tue, 31 Mar 2026 19:33:03 +0530 Subject: [PATCH 01/28] feat: implement dynamic sitemap with page router and updated wrt build failure Signed-off-by: amaan-bhati --- lib/sitemap.ts | 162 +++++ package-lock.json | 2 +- pages/_document.tsx | 3 +- pages/sitemap.xml.ts | 21 + public/robots.txt | 2 +- public/sitemap.xml | 1651 ------------------------------------------ 6 files changed, 187 insertions(+), 1654 deletions(-) create mode 100644 lib/sitemap.ts create mode 100644 pages/sitemap.xml.ts delete mode 100644 public/sitemap.xml diff --git a/lib/sitemap.ts b/lib/sitemap.ts new file mode 100644 index 00000000..80bc09b1 --- /dev/null +++ b/lib/sitemap.ts @@ -0,0 +1,162 @@ +import { getAllPostsForCommunity, getAllPostsForTechnology, getAllTags } from "./api"; +import { SITE_URL } from "./structured-data"; +import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; + +type PostEdge = { + node: { + slug?: string; + date?: string; + ppmaAuthorName?: string; + }; +}; + +type SitemapEntry = { + loc: string; + lastmod?: string; + changefreq?: "daily" | "weekly" | "monthly"; + priority?: number; +}; + +type PaginatedPostsResponse = { + edges?: PostEdge[]; + pageInfo?: { + hasNextPage?: boolean; + endCursor?: string | null; + }; +}; + +const STATIC_ROUTES: SitemapEntry[] = [ + { loc: SITE_URL, changefreq: "daily", priority: 1.0 }, + { loc: `${SITE_URL}/technology`, changefreq: "daily", priority: 0.9 }, + { loc: `${SITE_URL}/community`, changefreq: "daily", priority: 0.9 }, + { loc: `${SITE_URL}/authors`, changefreq: "weekly", priority: 0.8 }, + { loc: `${SITE_URL}/tag`, changefreq: "weekly", priority: 0.8 }, + { loc: `${SITE_URL}/search`, changefreq: "weekly", priority: 0.6 }, + { loc: `${SITE_URL}/community/search`, changefreq: "weekly", priority: 0.6 }, +]; + +async function getAllCategoryPosts( + fetchPage: (preview?: boolean, after?: string | null) => Promise +) { + const edges: PostEdge[] = []; + let after: string | null = null; + let hasNextPage = true; + + while (hasNextPage) { + const page = await fetchPage(false, after); + edges.push(...(page?.edges || [])); + hasNextPage = Boolean(page?.pageInfo?.hasNextPage); + after = page?.pageInfo?.endCursor || null; + } + + return edges; +} + +function toIsoDate(value?: string) { + if (!value) { + return undefined; + } + + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString(); +} + +function escapeXml(value: string) { + return value + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(//g, ">"); +} + +function dedupeEntries(entries: SitemapEntry[]) { + return Array.from(new Map(entries.map((entry) => [entry.loc, entry])).values()); +} + +export async function getSitemapEntries() { + const [technologyPosts, communityPosts, tags] = await Promise.all([ + getAllCategoryPosts(getAllPostsForTechnology), + getAllCategoryPosts(getAllPostsForCommunity), + getAllTags(), + ]); + + const allPosts = [...technologyPosts, ...communityPosts]; + const latestPostDate = allPosts.reduce((latest, edge) => { + const current = toIsoDate(edge?.node?.date); + if (!current) { + return latest; + } + + if (!latest || current > latest) { + return current; + } + + return latest; + }, undefined); + + const postEntries: SitemapEntry[] = [ + ...technologyPosts.map(({ node }) => ({ + loc: `${SITE_URL}/technology/${encodeURIComponent(node.slug || "")}`, + lastmod: toIsoDate(node.date), + changefreq: "weekly" as const, + priority: 0.8, + })), + ...communityPosts.map(({ node }) => ({ + loc: `${SITE_URL}/community/${encodeURIComponent(node.slug || "")}`, + lastmod: toIsoDate(node.date), + changefreq: "weekly" as const, + priority: 0.8, + })), + ].filter((entry) => !entry.loc.endsWith("/")); + + const authorEntries: SitemapEntry[] = Array.from( + new Set( + allPosts + .map(({ node }) => node?.ppmaAuthorName) + .filter((authorName): authorName is string => Boolean(authorName?.trim())) + .map((authorName) => sanitizeAuthorSlug(authorName)) + .filter(Boolean) + ) + ).map((authorSlug) => ({ + loc: `${SITE_URL}/authors/${authorSlug}`, + changefreq: "weekly", + priority: 0.7, + })); + + const tagEntries: SitemapEntry[] = (tags || []) + .map((tag) => tag?.name) + .filter((tagName): tagName is string => Boolean(tagName?.trim())) + .map((tagName) => ({ + loc: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, + changefreq: "weekly" as const, + priority: 0.7, + })); + + const staticEntries = STATIC_ROUTES.map((entry) => ({ + ...entry, + lastmod: latestPostDate, + })); + + return dedupeEntries([...staticEntries, ...postEntries, ...authorEntries, ...tagEntries]); +} + +export async function generateSitemapXml() { + const entries = await getSitemapEntries(); + + const body = entries + .map((entry) => { + const parts = [ + `${escapeXml(entry.loc)}`, + entry.lastmod ? `${escapeXml(entry.lastmod)}` : "", + entry.changefreq ? `${entry.changefreq}` : "", + typeof entry.priority === "number" ? `${entry.priority.toFixed(1)}` : "", + ].filter(Boolean); + + return `${parts.join("")}`; + }) + .join(""); + + return `` + + `${body}`; +} diff --git a/package-lock.json b/package-lock.json index 69a99930..8306b6b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "tailwindcss": "^3.0.24" }, "engines": { - "node": ">=18 <19" + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/pages/_document.tsx b/pages/_document.tsx index bffba660..83811c20 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,3 +1,4 @@ +import Script from 'next/script'; import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { @@ -28,4 +29,4 @@ export default function Document() { ); -} \ No newline at end of file +} diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts new file mode 100644 index 00000000..0224e064 --- /dev/null +++ b/pages/sitemap.xml.ts @@ -0,0 +1,21 @@ +import { GetServerSideProps } from "next"; +import { generateSitemapXml } from "../lib/sitemap"; + +function SitemapXml() { + return null; +} + +export const getServerSideProps: GetServerSideProps = async ({ res }) => { + const sitemap = await generateSitemapXml(); + + res.setHeader("Content-Type", "application/xml"); + res.setHeader("Cache-Control", "s-maxage=3600, stale-while-revalidate=86400"); + res.write(sitemap); + res.end(); + + return { + props: {}, + }; +}; + +export default SitemapXml; diff --git a/public/robots.txt b/public/robots.txt index f0be87df..403e9600 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -7,4 +7,4 @@ Disallow: /blog.keploy.io/ Disallow: /telemetry.keploy.io/ Disallow: /student.keploy.io/ Disallow: /wp/ -Sitemap: https://www.keploy.io/blog/sitemap.xml \ No newline at end of file +Sitemap: https://keploy.io/blog/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index fd919561..00000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,1651 +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/gittogether - 2024-03-07T09:25:36+00:00 - 0.64 - - - https://keploy.io/about - 2024-03-07T09:25:36+00:00 - 0.64 - - - https://keploy.io/security - 2024-03-07T09:25:36+00:00 - 0.64 - - - https://keploy.io/privacy-policy - 2024-03-07T09:25:36+00:00 - 0.64 - - - 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/technology/canary-testing-a-comprehensive-guide-for-developers - 2024-03-07T09:25:36+00:00 - 0.64 - - - https://keploy.io/blog/technology/mock-vs-stub-vs-fake-understand-the-difference - 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/reset-password - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/signup - 2024-03-07T09:25:36+00:00 - 0.51 - - - 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 - - - https://keploy.io/blog/community/mongodb-in-mock-mode-acting-the-server-part - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/technology/writing-test-cases-for-cron-jobs-testing - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/community/capture-grpc-traffic-going-out-from-a-server - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/community/automated-e2e-tests-using-property-based-testing-part-ii - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/community/automated-end-to-end-tests-using-property-based-testing-part-i - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/community/integration-vs-e2e-testing-what-worked-for-me-as-a-charm - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/blog/community/go-mocks-and-stubs-made-easy - 2024-03-07T09:25:36+00:00 - 0.51 - - - https://keploy.io/signin - 2024-03-07T09:25:36+00:00 - 0.41 - - - \ No newline at end of file From 204b2a9f4e26a2adc9d6c3965cf86401cef67d09 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 1 Apr 2026 12:43:03 +0530 Subject: [PATCH 02/28] feat: migration implementation of app sitemap.ts in page router Signed-off-by: amaan-bhati --- lib/sitemap.ts | 344 ++++++++++++++++++++++++++++++++----------- pages/sitemap.xml.ts | 2 +- 2 files changed, 257 insertions(+), 89 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 80bc09b1..8ef9c605 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -1,55 +1,168 @@ -import { getAllPostsForCommunity, getAllPostsForTechnology, getAllTags } from "./api"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; -type PostEdge = { - node: { - slug?: string; - date?: string; - ppmaAuthorName?: string; - }; -}; +const WP_API_URL = + process.env.WORDPRESS_API_URL || + process.env.NEXT_PUBLIC_WORDPRESS_API_URL || + "https://wp.keploy.io/graphql"; + +type SitemapChangeFrequency = "daily" | "weekly" | "monthly"; -type SitemapEntry = { - loc: string; - lastmod?: string; - changefreq?: "daily" | "weekly" | "monthly"; +export type SitemapEntry = { + url: string; + lastModified?: string; + changeFrequency?: SitemapChangeFrequency; priority?: number; }; -type PaginatedPostsResponse = { - edges?: PostEdge[]; - pageInfo?: { - hasNextPage?: boolean; - endCursor?: string | null; +type CategoryRoute = "technology" | "community"; + +type SitemapPost = { + slug: string; + modified?: string; + authorName?: string; + tags: string[]; +}; + +type GraphQLResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; + +type PostsQueryResponse = { + posts?: { + edges?: Array<{ + node?: { + slug?: string; + modified?: string; + ppmaAuthorName?: string; + tags?: { + edges?: Array<{ + node?: { + name?: string; + }; + }>; + }; + }; + }>; + pageInfo?: { + hasNextPage?: boolean; + endCursor?: string | null; + }; }; }; -const STATIC_ROUTES: SitemapEntry[] = [ - { loc: SITE_URL, changefreq: "daily", priority: 1.0 }, - { loc: `${SITE_URL}/technology`, changefreq: "daily", priority: 0.9 }, - { loc: `${SITE_URL}/community`, changefreq: "daily", priority: 0.9 }, - { loc: `${SITE_URL}/authors`, changefreq: "weekly", priority: 0.8 }, - { loc: `${SITE_URL}/tag`, changefreq: "weekly", priority: 0.8 }, - { loc: `${SITE_URL}/search`, changefreq: "weekly", priority: 0.6 }, - { loc: `${SITE_URL}/community/search`, changefreq: "weekly", priority: 0.6 }, +const STATIC_ROUTES: Array> = [ + { 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 }, ]; -async function getAllCategoryPosts( - fetchPage: (preview?: boolean, after?: string | null) => Promise -) { - const edges: PostEdge[] = []; - let after: string | null = null; +async function fetchGraphQL(query: string, variables: Record = {}) { + const response = await fetch(WP_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error(`WordPress GraphQL request failed: ${response.status} ${response.statusText}`); + } + + const json = (await response.json()) as GraphQLResponse; + + if (json.errors?.length) { + const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); + throw new Error(message || "WordPress GraphQL returned errors"); + } + + if (!json.data) { + throw new Error("WordPress GraphQL returned no data"); + } + + return json.data; +} + +async function fetchAllPostsByCategory(categoryName: CategoryRoute) { + const posts: SitemapPost[] = []; let hasNextPage = true; + let after: string | null = null; while (hasNextPage) { - const page = await fetchPage(false, after); - edges.push(...(page?.edges || [])); - hasNextPage = Boolean(page?.pageInfo?.hasNextPage); - after = page?.pageInfo?.endCursor || null; + const data = await fetchGraphQL( + ` + query SitemapPosts($categoryName: String!, $after: String) { + posts( + first: 100 + after: $after + where: { + categoryName: $categoryName + orderby: { field: MODIFIED, order: DESC } + } + ) { + edges { + node { + slug + modified + ppmaAuthorName + tags { + edges { + node { + name + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + { after, categoryName } + ); + + const edges = data.posts?.edges || []; + + for (const edge of edges) { + const node = edge?.node; + if (!node?.slug) { + continue; + } + + posts.push({ + slug: node.slug, + modified: node.modified, + authorName: node.ppmaAuthorName, + tags: + node.tags?.edges + ?.map((tagEdge) => tagEdge?.node?.name?.trim()) + .filter((tagName): tagName is string => Boolean(tagName)) || [], + }); + } + + hasNextPage = Boolean(data.posts?.pageInfo?.hasNextPage); + after = data.posts?.pageInfo?.endCursor || null; } - return edges; + return posts; +} + +async function safeFetchAllPostsByCategory(categoryName: CategoryRoute) { + try { + return await fetchAllPostsByCategory(categoryName); + } catch (error) { + console.error(`Sitemap fetch failed for category "${categoryName}":`, error); + return []; + } } function toIsoDate(value?: string) { @@ -61,6 +174,18 @@ function toIsoDate(value?: string) { 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; +} + function escapeXml(value: string) { return value .replace(/&/g, "&") @@ -71,19 +196,12 @@ function escapeXml(value: string) { } function dedupeEntries(entries: SitemapEntry[]) { - return Array.from(new Map(entries.map((entry) => [entry.loc, entry])).values()); + return Array.from(new Map(entries.map((entry) => [entry.url, entry])).values()); } -export async function getSitemapEntries() { - const [technologyPosts, communityPosts, tags] = await Promise.all([ - getAllCategoryPosts(getAllPostsForTechnology), - getAllCategoryPosts(getAllPostsForCommunity), - getAllTags(), - ]); - - const allPosts = [...technologyPosts, ...communityPosts]; - const latestPostDate = allPosts.reduce((latest, edge) => { - const current = toIsoDate(edge?.node?.date); +function getLatestModified(posts: SitemapPost[]) { + return posts.reduce((latest, post) => { + const current = toIsoDate(post.modified); if (!current) { return latest; } @@ -94,62 +212,107 @@ export async function getSitemapEntries() { return latest; }, undefined); +} + +function buildPostEntries(posts: SitemapPost[], route: CategoryRoute): SitemapEntry[] { + return posts + .filter((post) => Boolean(post.slug)) + .map((post) => ({ + url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`, + lastModified: toIsoDate(post.modified), + changeFrequency: "weekly" as const, + priority: isRecent(post.modified) ? 0.8 : 0.5, + })); +} + +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 postEntries: SitemapEntry[] = [ - ...technologyPosts.map(({ node }) => ({ - loc: `${SITE_URL}/technology/${encodeURIComponent(node.slug || "")}`, - lastmod: toIsoDate(node.date), - changefreq: "weekly" as const, - priority: 0.8, - })), - ...communityPosts.map(({ node }) => ({ - loc: `${SITE_URL}/community/${encodeURIComponent(node.slug || "")}`, - lastmod: toIsoDate(node.date), - changefreq: "weekly" as const, - priority: 0.8, - })), - ].filter((entry) => !entry.loc.endsWith("/")); - - const authorEntries: SitemapEntry[] = Array.from( - new Set( - allPosts - .map(({ node }) => node?.ppmaAuthorName) - .filter((authorName): authorName is string => Boolean(authorName?.trim())) - .map((authorName) => sanitizeAuthorSlug(authorName)) - .filter(Boolean) - ) - ).map((authorSlug) => ({ - loc: `${SITE_URL}/authors/${authorSlug}`, - changefreq: "weekly", + 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", priority: 0.7, })); +} - const tagEntries: SitemapEntry[] = (tags || []) - .map((tag) => tag?.name) - .filter((tagName): tagName is string => Boolean(tagName?.trim())) - .map((tagName) => ({ - loc: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, - changefreq: "weekly" as const, - priority: 0.7, - })); +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()).map(([tagName, lastModified]) => ({ + url: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, + lastModified, + changeFrequency: "weekly", + priority: 0.7, + })); +} + +export async function getSitemapEntries() { + // Fetch sequentially to reduce burst pressure on WPGraphQL and keep sitemap generation resilient. + const technologyPosts = await safeFetchAllPostsByCategory("technology"); + const communityPosts = await safeFetchAllPostsByCategory("community"); + + const allPosts = [...technologyPosts, ...communityPosts]; + const latestPostModified = getLatestModified(allPosts) || new Date().toISOString(); const staticEntries = STATIC_ROUTES.map((entry) => ({ ...entry, - lastmod: latestPostDate, + lastModified: latestPostModified, })); - return dedupeEntries([...staticEntries, ...postEntries, ...authorEntries, ...tagEntries]); -} + const entries = [ + ...staticEntries, + ...buildPostEntries(technologyPosts, "technology"), + ...buildPostEntries(communityPosts, "community"), + ...buildAuthorEntries(allPosts), + ...buildTagEntries(allPosts), + ]; -export async function generateSitemapXml() { - const entries = await getSitemapEntries(); + return dedupeEntries(entries); +} +export function serializeSitemap(entries: SitemapEntry[]) { const body = entries .map((entry) => { const parts = [ - `${escapeXml(entry.loc)}`, - entry.lastmod ? `${escapeXml(entry.lastmod)}` : "", - entry.changefreq ? `${entry.changefreq}` : "", + `${escapeXml(entry.url)}`, + entry.lastModified ? `${escapeXml(entry.lastModified)}` : "", + entry.changeFrequency ? `${entry.changeFrequency}` : "", typeof entry.priority === "number" ? `${entry.priority.toFixed(1)}` : "", ].filter(Boolean); @@ -160,3 +323,8 @@ export async function generateSitemapXml() { return `` + `${body}`; } + +export async function generateSitemapXml() { + const entries = await getSitemapEntries(); + return serializeSitemap(entries); +} diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 0224e064..557cd0d0 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -9,7 +9,7 @@ export const getServerSideProps: GetServerSideProps = async ({ res }) => { const sitemap = await generateSitemapXml(); res.setHeader("Content-Type", "application/xml"); - res.setHeader("Cache-Control", "s-maxage=3600, stale-while-revalidate=86400"); + res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=86400"); res.write(sitemap); res.end(); From 04fa5f2d0a601b70914ec97eb16f61a5e182d7c5 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 1 Apr 2026 14:09:23 +0530 Subject: [PATCH 03/28] feat: fix edge cases and error wp 502 error handling Signed-off-by: amaan-bhati --- lib/sitemap.ts | 253 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 188 insertions(+), 65 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 8ef9c605..3a996adb 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -1,3 +1,5 @@ +import { promises as fs } from "fs"; +import path from "path"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; @@ -6,7 +8,15 @@ const WP_API_URL = process.env.NEXT_PUBLIC_WORDPRESS_API_URL || "https://wp.keploy.io/graphql"; +const SITEMAP_SNAPSHOT_PATH = path.join("/tmp", "keploy-blog-sitemap.xml"); +const FETCH_RETRY_LIMIT = 6; +const FETCH_RETRY_DELAY_MS = 2000; +const FETCH_TIMEOUT_MS = 25000; +const POSTS_PAGE_SIZE = 50; +const PAGE_SETTLE_DELAY_MS = 250; + type SitemapChangeFrequency = "daily" | "weekly" | "monthly"; +type CategoryRoute = "technology" | "community"; export type SitemapEntry = { url: string; @@ -15,21 +25,20 @@ export type SitemapEntry = { priority?: number; }; -type CategoryRoute = "technology" | "community"; +type GraphQLResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; type SitemapPost = { slug: string; modified?: string; authorName?: string; tags: string[]; + routes: CategoryRoute[]; }; -type GraphQLResponse = { - data?: T; - errors?: Array<{ message?: string }>; -}; - -type PostsQueryResponse = { +type AllPostsQueryResponse = { posts?: { edges?: Array<{ node?: { @@ -43,6 +52,14 @@ type PostsQueryResponse = { }; }>; }; + categories?: { + edges?: Array<{ + node?: { + name?: string; + slug?: string; + }; + }>; + }; }; }>; pageInfo?: { @@ -62,47 +79,98 @@ const STATIC_ROUTES: Array> = [ { url: `${SITE_URL}/community/search`, changeFrequency: "weekly", priority: 0.6 }, ]; +let lastSuccessfulSitemapXml: string | null = null; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isRetryableStatus(status: number) { + return [408, 429, 500, 502, 503, 504].includes(status); +} + async function fetchGraphQL(query: string, variables: Record = {}) { - const response = await fetch(WP_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ query, variables }), - }); - - if (!response.ok) { - throw new Error(`WordPress GraphQL request failed: ${response.status} ${response.statusText}`); - } + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= FETCH_RETRY_LIMIT; attempt += 1) { + try { + const response = await fetch(WP_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query, variables }), + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }); + + if (!response.ok) { + if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { + await sleep(FETCH_RETRY_DELAY_MS * attempt); + continue; + } + + throw new Error(`WordPress GraphQL request failed: ${response.status} ${response.statusText}`); + } + + const json = (await response.json()) as GraphQLResponse; + + if (json.errors?.length) { + const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); + throw new Error(message || "WordPress GraphQL returned errors"); + } + + if (!json.data) { + throw new Error("WordPress GraphQL returned no data"); + } - const json = (await response.json()) as GraphQLResponse; + return json.data; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); - if (json.errors?.length) { - const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); - throw new Error(message || "WordPress GraphQL returned errors"); + if (attempt < FETCH_RETRY_LIMIT) { + await sleep(FETCH_RETRY_DELAY_MS * attempt); + continue; + } + } } - if (!json.data) { - throw new Error("WordPress GraphQL returned no data"); + throw lastError || new Error("WordPress GraphQL request failed"); +} + +function mapCategoriesToRoutes( + categories?: AllPostsQueryResponse["posts"]["edges"][number]["node"]["categories"] +) { + 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 json.data; + return Array.from(routes); } -async function fetchAllPostsByCategory(categoryName: CategoryRoute) { +async function fetchAllPosts() { const posts: SitemapPost[] = []; let hasNextPage = true; let after: string | null = null; while (hasNextPage) { - const data = await fetchGraphQL( + const data = await fetchGraphQL( ` - query SitemapPosts($categoryName: String!, $after: String) { + query SitemapPosts($after: String) { posts( - first: 100 + first: ${POSTS_PAGE_SIZE} after: $after where: { - categoryName: $categoryName orderby: { field: MODIFIED, order: DESC } } ) { @@ -118,6 +186,14 @@ async function fetchAllPostsByCategory(categoryName: CategoryRoute) { } } } + categories { + edges { + node { + name + slug + } + } + } } } pageInfo { @@ -127,7 +203,7 @@ async function fetchAllPostsByCategory(categoryName: CategoryRoute) { } } `, - { after, categoryName } + { after } ); const edges = data.posts?.edges || []; @@ -138,10 +214,16 @@ async function fetchAllPostsByCategory(categoryName: CategoryRoute) { 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((tagEdge) => tagEdge?.node?.name?.trim()) @@ -151,20 +233,15 @@ async function fetchAllPostsByCategory(categoryName: CategoryRoute) { hasNextPage = Boolean(data.posts?.pageInfo?.hasNextPage); after = data.posts?.pageInfo?.endCursor || null; + + if (hasNextPage) { + await sleep(PAGE_SETTLE_DELAY_MS); + } } return posts; } -async function safeFetchAllPostsByCategory(categoryName: CategoryRoute) { - try { - return await fetchAllPostsByCategory(categoryName); - } catch (error) { - console.error(`Sitemap fetch failed for category "${categoryName}":`, error); - return []; - } -} - function toIsoDate(value?: string) { if (!value) { return undefined; @@ -214,18 +291,18 @@ function getLatestModified(posts: SitemapPost[]) { }, undefined); } -function buildPostEntries(posts: SitemapPost[], route: CategoryRoute): SitemapEntry[] { - return posts - .filter((post) => Boolean(post.slug)) - .map((post) => ({ +function buildPostEntries(posts: SitemapPost[]) { + return posts.flatMap((post) => + post.routes.map((route) => ({ url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`, lastModified: toIsoDate(post.modified), changeFrequency: "weekly" as const, priority: isRecent(post.modified) ? 0.8 : 0.5, - })); + })) + ); } -function buildAuthorEntries(posts: SitemapPost[]): SitemapEntry[] { +function buildAuthorEntries(posts: SitemapPost[]) { const authorMap = new Map(); for (const post of posts) { @@ -250,12 +327,12 @@ function buildAuthorEntries(posts: SitemapPost[]): SitemapEntry[] { return Array.from(authorMap.entries()).map(([authorSlug, lastModified]) => ({ url: `${SITE_URL}/authors/${authorSlug}`, lastModified, - changeFrequency: "weekly", + changeFrequency: "weekly" as const, priority: 0.7, })); } -function buildTagEntries(posts: SitemapPost[]): SitemapEntry[] { +function buildTagEntries(posts: SitemapPost[]) { const tagMap = new Map(); for (const post of posts) { @@ -277,33 +354,42 @@ function buildTagEntries(posts: SitemapPost[]): SitemapEntry[] { return Array.from(tagMap.entries()).map(([tagName, lastModified]) => ({ url: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, lastModified, - changeFrequency: "weekly", + changeFrequency: "weekly" as const, priority: 0.7, })); } -export async function getSitemapEntries() { - // Fetch sequentially to reduce burst pressure on WPGraphQL and keep sitemap generation resilient. - const technologyPosts = await safeFetchAllPostsByCategory("technology"); - const communityPosts = await safeFetchAllPostsByCategory("community"); +function assertFullSitemap(posts: SitemapPost[]) { + if (!posts.length) { + throw new Error("Sitemap generation returned zero posts"); + } + + const technologyCount = posts.filter((post) => post.routes.includes("technology")).length; + const communityCount = posts.filter((post) => post.routes.includes("community")).length; + + if (!technologyCount || !communityCount) { + throw new Error( + `Sitemap generation incomplete: technology=${technologyCount}, community=${communityCount}` + ); + } +} - const allPosts = [...technologyPosts, ...communityPosts]; - const latestPostModified = getLatestModified(allPosts) || new Date().toISOString(); +export async function getSitemapEntries() { + const posts = await fetchAllPosts(); + assertFullSitemap(posts); + const latestPostModified = getLatestModified(posts) || new Date().toISOString(); const staticEntries = STATIC_ROUTES.map((entry) => ({ ...entry, lastModified: latestPostModified, })); - const entries = [ + return dedupeEntries([ ...staticEntries, - ...buildPostEntries(technologyPosts, "technology"), - ...buildPostEntries(communityPosts, "community"), - ...buildAuthorEntries(allPosts), - ...buildTagEntries(allPosts), - ]; - - return dedupeEntries(entries); + ...buildPostEntries(posts), + ...buildAuthorEntries(posts), + ...buildTagEntries(posts), + ]); } export function serializeSitemap(entries: SitemapEntry[]) { @@ -324,7 +410,44 @@ export function serializeSitemap(entries: SitemapEntry[]) { `${body}`; } +async function readPersistedSitemapSnapshot() { + try { + return await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); + } catch { + return null; + } +} + +async function persistSitemapSnapshot(xml: string) { + try { + await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); + } catch (error) { + console.error("Failed to persist sitemap snapshot:", error); + } +} + export async function generateSitemapXml() { - const entries = await getSitemapEntries(); - return serializeSitemap(entries); + try { + const entries = await getSitemapEntries(); + const xml = serializeSitemap(entries); + + lastSuccessfulSitemapXml = xml; + await persistSitemapSnapshot(xml); + + return xml; + } catch (error) { + console.error("Fresh sitemap generation failed, trying last successful snapshot:", error); + + if (lastSuccessfulSitemapXml) { + return lastSuccessfulSitemapXml; + } + + const persistedSnapshot = await readPersistedSitemapSnapshot(); + if (persistedSnapshot) { + lastSuccessfulSitemapXml = persistedSnapshot; + return persistedSnapshot; + } + + throw error; + } } From 97af5d55ec124c3504553808b4da64a9012ad7e8 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 1 Apr 2026 15:00:15 +0530 Subject: [PATCH 04/28] chore: test commit to check build pipeline Signed-off-by: amaan-bhati --- pages/_document.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/_document.tsx b/pages/_document.tsx index 28d34cc7..6fe73bfe 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -29,4 +29,4 @@ export default function Document() { ); -} +} \ No newline at end of file From aff778d619537a3cab2e5d75c31aef1761be7d62 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 1 Apr 2026 17:18:12 +0530 Subject: [PATCH 05/28] feat: add vercel cronjob, refreshsitemap, edge cases, auth and snapshots Signed-off-by: amaan-bhati --- lib/sitemap.ts | 53 ++++++++++++++++++++++++++----- pages/api/cron/refresh-sitemap.ts | 36 +++++++++++++++++++++ vercel.json | 10 ++++++ 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 pages/api/cron/refresh-sitemap.ts diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 3a996adb..417aa011 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -80,6 +80,13 @@ const STATIC_ROUTES: Array> = [ ]; let lastSuccessfulSitemapXml: string | null = null; +let refreshSitemapPromise: Promise | null = null; + +export type SitemapRefreshResult = { + entryCount: number; + generatedAt: string; + xml: string; +}; function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -412,12 +419,23 @@ export function serializeSitemap(entries: SitemapEntry[]) { async function readPersistedSitemapSnapshot() { try { - return await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); + const xml = await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); + return isValidSitemapXml(xml) ? xml : null; } catch { return null; } } +function isValidSitemapXml(xml: string) { + const normalized = xml.trim(); + + return ( + normalized.startsWith(``) && + normalized.includes(``) && + normalized.endsWith(``) + ); +} + async function persistSitemapSnapshot(xml: string) { try { await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); @@ -426,15 +444,34 @@ async function persistSitemapSnapshot(xml: string) { } } -export async function generateSitemapXml() { - try { - const entries = await getSitemapEntries(); - const xml = serializeSitemap(entries); +export async function refreshSitemapSnapshot(): Promise { + if (!refreshSitemapPromise) { + refreshSitemapPromise = (async () => { + const entries = await getSitemapEntries(); + const xml = serializeSitemap(entries); - lastSuccessfulSitemapXml = xml; - await persistSitemapSnapshot(xml); + lastSuccessfulSitemapXml = xml; + await persistSitemapSnapshot(xml); - return xml; + return { + entryCount: entries.length, + generatedAt: new Date().toISOString(), + xml, + }; + })(); + + refreshSitemapPromise.finally(() => { + refreshSitemapPromise = null; + }); + } + + return refreshSitemapPromise; +} + +export async function generateSitemapXml() { + try { + const result = await refreshSitemapSnapshot(); + return result.xml; } catch (error) { console.error("Fresh sitemap generation failed, trying last successful snapshot:", error); diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts new file mode 100644 index 00000000..aea29c10 --- /dev/null +++ b/pages/api/cron/refresh-sitemap.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { refreshSitemapSnapshot } from "../../../lib/sitemap"; + +export default async function refreshSitemap( + req: NextApiRequest, + res: NextApiResponse +) { + const authHeader = req.headers.authorization; + const expectedSecret = process.env.CRON_SECRET; + + if (!expectedSecret || authHeader !== `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" }); + } + + try { + const result = await refreshSitemapSnapshot(); + + return res.status(200).json({ + ok: true, + entryCount: result.entryCount, + generatedAt: result.generatedAt, + }); + } catch (error) { + console.error("Scheduled sitemap refresh failed:", error); + + return res.status(500).json({ + ok: false, + message: "Sitemap refresh failed", + }); + } +} diff --git a/vercel.json b/vercel.json index f7029aba..b129ca2b 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,10 @@ { + "crons": [ + { + "path": "/blog/api/cron/refresh-sitemap", + "schedule": "0 0 * * *" + } + ], "redirects": [ { "source": "/blog/community/everything-you-need-to-know-about-api-testing", @@ -37,6 +43,10 @@ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" + } + ] + }, + { "source": "/blog/(.*)", "headers": [ { From 4c2eeb1894498e38c95e1b975fca34762a6acddb Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 1 Apr 2026 21:31:03 +0530 Subject: [PATCH 06/28] feat: add onew more cron job for auto google indexing using api Signed-off-by: amaan-bhati --- lib/google-search-console.ts | 136 ++++++++++++++++++++++++++++++ pages/api/cron/refresh-sitemap.ts | 42 +++++++++ 2 files changed, 178 insertions(+) create mode 100644 lib/google-search-console.ts diff --git a/lib/google-search-console.ts b/lib/google-search-console.ts new file mode 100644 index 00000000..8d3b4d84 --- /dev/null +++ b/lib/google-search-console.ts @@ -0,0 +1,136 @@ +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; + +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, + }), + }); + + if (!response.ok) { + throw new Error(`Google OAuth token request failed: ${response.status} ${response.statusText}`); + } + + 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}`, + }, + } + ); + + 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/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index aea29c10..65eae440 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -1,4 +1,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { + isSearchConsoleSubmissionConfigured, + submitSitemapToSearchConsole, +} from "../../../lib/google-search-console"; import { refreshSitemapSnapshot } from "../../../lib/sitemap"; export default async function refreshSitemap( @@ -19,11 +23,49 @@ export default async function refreshSitemap( try { const result = await refreshSitemapSnapshot(); + let searchConsole: + | { + submitted: boolean; + submittedAt?: string; + sitemapUrl?: string; + siteUrl?: string; + skipped?: boolean; + message?: string; + } + | undefined; + + if (isSearchConsoleSubmissionConfigured()) { + try { + const submission = await submitSitemapToSearchConsole(); + searchConsole = { + submitted: true, + submittedAt: submission.submittedAt, + sitemapUrl: submission.sitemapUrl, + siteUrl: submission.siteUrl, + }; + } catch (error) { + console.error("Google Search Console sitemap submission failed:", error); + searchConsole = { + submitted: false, + message: + error instanceof Error + ? error.message + : "Google Search Console sitemap submission failed", + }; + } + } else { + searchConsole = { + submitted: false, + skipped: true, + message: "Google Search Console submission is not configured", + }; + } return res.status(200).json({ ok: true, entryCount: result.entryCount, generatedAt: result.generatedAt, + searchConsole, }); } catch (error) { console.error("Scheduled sitemap refresh failed:", error); From aeccd7930b0605fd420c6220296d7338ac7b7088 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Tue, 7 Apr 2026 09:36:49 +0530 Subject: [PATCH 07/28] feat: add comments and fix copilot reviews Signed-off-by: amaan-bhati --- lib/google-search-console.ts | 10 +- lib/sitemap.ts | 185 +++++++++++++++++++++++++++++- pages/api/cron/refresh-sitemap.ts | 36 +++++- pages/sitemap.xml.ts | 11 ++ 4 files changed, 232 insertions(+), 10 deletions(-) diff --git a/lib/google-search-console.ts b/lib/google-search-console.ts index 8d3b4d84..1f163efa 100644 --- a/lib/google-search-console.ts +++ b/lib/google-search-console.ts @@ -5,6 +5,7 @@ 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(); @@ -69,6 +70,7 @@ async function fetchGoogleAccessToken() { grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion, }), + signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS), }); if (!response.ok) { @@ -97,8 +99,8 @@ function getSitemapUrl() { 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() + process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY?.trim() && + process.env.GOOGLE_SEARCH_CONSOLE_SITE_URL?.trim() ); } @@ -116,14 +118,14 @@ export async function submitSitemapToSearchConsole() { 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}` : "" + `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : "" }` ); } diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 417aa011..72e9ec96 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -3,38 +3,76 @@ import path from "path"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; +// choose the wordpress graphql endpoint in this order: +// server-only env for production or local server use +// public env as a fallback if the server env is missing +// hardcoded endpoint so local development still works out of the box + +// this value is the single source used for every wordpress fetch in the sitemap flow. const WP_API_URL = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL || "https://wp.keploy.io/graphql"; +// store the last successful xml in /tmp so a later failed refresh can fall back to it +// this is runtime local storage, not durable database storage, so it helps during the +// life of a runtime instance but is not guaranteed across instance replacement const SITEMAP_SNAPSHOT_PATH = path.join("/tmp", "keploy-blog-sitemap.xml"); + +// how many times a single wordpress request can be retried before failing. const FETCH_RETRY_LIMIT = 6; + +// base retry delay in milliseconds. the actual wait grows with each attempt. const FETCH_RETRY_DELAY_MS = 2000; + +// fail a single wordpress request if it hangs too long. const FETCH_TIMEOUT_MS = 25000; + +// fetch posts in pages of 50 to avoid overloading wordpress with huge payloads. const POSTS_PAGE_SIZE = 50; + +// wait briefly between pages so the crawl is less aggressive on wpgraphql. const PAGE_SETTLE_DELAY_MS = 250; 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; }; type GraphQLResponse = { + // graphql returns successful payloads under data. data?: T; + + // graphql can also return logical errors even when the http request itself succeeded. errors?: Array<{ message?: string }>; }; type SitemapPost = { + // wordpress post slug used to build the frontend url. slug: string; + + // wordpress last updated timestamp for the post. modified?: string; + + // author name returned by wordpress for this post. authorName?: string; + + // tag names attached to the post. tags: string[]; + + // local route namespaces this post belongs to after category mapping. routes: CategoryRoute[]; }; @@ -69,7 +107,17 @@ type AllPostsQueryResponse = { }; }; +export type PostCategoryConnection = { + edges?: Array<{ + node?: { + name?: string | null; + slug?: string | null; + } | null; + } | null> | null; +} | null; + 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 }, @@ -79,7 +127,12 @@ const STATIC_ROUTES: Array> = [ { url: `${SITE_URL}/community/search`, changeFrequency: "weekly", priority: 0.6 }, ]; +// keep the latest successful sitemap in memory so request-time fallback is instant +// when the same runtime handles a later failure. let lastSuccessfulSitemapXml: string | null = null; + +// hold the in-flight refresh promise so concurrent callers share one crawl instead +// of each starting an independent wordpress fetch sequence. let refreshSitemapPromise: Promise | null = null; export type SitemapRefreshResult = { @@ -92,15 +145,27 @@ function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +// retry statuses that are commonly temporary: +// - timeouts +// - rate limits +// - transient upstream/server failures function isRetryableStatus(status: number) { return [408, 429, 500, 502, 503, 504].includes(status); } async function fetchGraphQL(query: string, variables: Record = {}) { + // remember the last seen error so the final thrown error is meaningful. let lastError: Error | null = null; for (let attempt = 1; attempt <= FETCH_RETRY_LIMIT; attempt += 1) { try { + // send the graphql request to wordpress. + // + // the body contains: + // - query: the graphql query string + // - variables: query variables such as the pagination cursor + // + // the abort signal enforces a hard timeout for the request. const response = await fetch(WP_API_URL, { method: "POST", headers: { @@ -112,29 +177,38 @@ async function fetchGraphQL(query: string, variables: Record if (!response.ok) { if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { + // back off before retrying so wordpress has a chance to recover and so + // we do not hammer the upstream on repeated transient failures. await sleep(FETCH_RETRY_DELAY_MS * attempt); continue; } + // for non-retryable failures, or when retries are exhausted, fail immediately. throw new Error(`WordPress GraphQL request failed: ${response.status} ${response.statusText}`); } + // parse the graphql response body after http success. const json = (await response.json()) as GraphQLResponse; if (json.errors?.length) { + // graphql can return application-level errors even when the http response is 200. const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); throw new Error(message || "WordPress GraphQL returned errors"); } if (!json.data) { + // a graphql response without data is not useful for sitemap generation. throw new Error("WordPress GraphQL returned no data"); } + // success path: return the typed graphql data. return json.data; } catch (error) { + // normalize unknown thrown values into an Error instance. lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < FETCH_RETRY_LIMIT) { + // retry fetch/network/timeout errors too, not just bad http statuses. await sleep(FETCH_RETRY_DELAY_MS * attempt); continue; } @@ -144,15 +218,19 @@ async function fetchGraphQL(query: string, variables: Record throw lastError || new Error("WordPress GraphQL request failed"); } -function mapCategoriesToRoutes( - categories?: AllPostsQueryResponse["posts"]["edges"][number]["node"]["categories"] -) { +function mapCategoriesToRoutes(categories?: PostCategoryConnection) { + // use a set so one post cannot produce duplicate routes even if wordpress returns + // the same category in multiple forms. const routes = new Set(); for (const edge of categories?.edges || []) { const slug = edge?.node?.slug?.trim()?.toLowerCase(); const name = edge?.node?.name?.trim()?.toLowerCase(); + // map wordpress category data to real frontend route namespaces. + // + // we support matching by either slug or name because wordpress content can be + // inconsistent across environments or editorial changes. if (slug === "technology" || name === "technology") { routes.add("technology"); } @@ -166,11 +244,28 @@ function mapCategoriesToRoutes( } async function fetchAllPosts() { + // collect only the posts that are eligible for sitemap inclusion. const posts: SitemapPost[] = []; + + // graphql cursor pagination state. let hasNextPage = true; let after: string | null = null; while (hasNextPage) { + // fetch one page at a time from wordpress. + // + // query design: + // - first: 50 keeps payload size reasonable + // - after: cursor for the next page + // - orderby modified desc: newer posts are returned first + // + // fields requested: + // - slug: needed to build the final frontend url + // - modified: used to build sitemap lastmod + // - ppmaAuthorName: used to derive author archive urls + // - tags: used to derive tag archive urls from included posts + // - categories: used to map a wordpress post to technology/community routes + // - pageInfo: needed to continue pagination until every post is processed const data = await fetchGraphQL( ` query SitemapPosts($after: String) { @@ -187,7 +282,7 @@ async function fetchAllPosts() { modified ppmaAuthorName tags { - edges { + edges {     node { name } @@ -218,14 +313,22 @@ async function fetchAllPosts() { for (const edge of edges) { const node = edge?.node; if (!node?.slug) { + // skip malformed wordpress records that do not have a usable slug. continue; } + // decide whether this wordpress post belongs to a supported sitemap route. + // if the categories do not map to technology/community, the post is excluded. const routes = mapCategoriesToRoutes(node.categories); if (!routes.length) { continue; } + // store the minimum data needed for later sitemap entry generation. + // + // note that we keep author and tag data here instead of running separate + // wordpress queries, because we only want author/tag pages backed by the + // exact set of posts that passed our inclusion rules. posts.push({ slug: node.slug, modified: node.modified, @@ -242,10 +345,12 @@ async function fetchAllPosts() { after = data.posts?.pageInfo?.endCursor || null; if (hasNextPage) { + // insert a small delay before fetching the next page to reduce pressure on wpgraphql. await sleep(PAGE_SETTLE_DELAY_MS); } } + // return the full eligible post set after every page has been processed. return posts; } @@ -254,6 +359,12 @@ function toIsoDate(value?: string) { return undefined; } + // convert the wordpress date string into a normalized iso string. + // + // why this exists: + // - wordpress may return a parseable date string format + // - the sitemap should emit a consistent machine-readable timestamp + // - invalid dates should quietly disappear instead of producing bad xml const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString(); } @@ -271,6 +382,7 @@ function isRecent(dateValue?: string, days = 30) { } function escapeXml(value: string) { + // escape xml-sensitive characters so urls and timestamps cannot break the xml document. return value .replace(/&/g, "&") .replace(/"/g, """) @@ -280,10 +392,15 @@ function escapeXml(value: string) { } function dedupeEntries(entries: SitemapEntry[]) { + // keep only one entry per url in case multiple generation paths produce the same final url. return Array.from(new Map(entries.map((entry) => [entry.url, entry])).values()); } function getLatestModified(posts: SitemapPost[]) { + // find the newest modified timestamp across all included posts. + // + // this value is later used for high-level listing pages like /blog, /technology, + // and /community so those pages appear updated when the newest underlying post changes. return posts.reduce((latest, post) => { const current = toIsoDate(post.modified); if (!current) { @@ -299,25 +416,38 @@ function getLatestModified(posts: SitemapPost[]) { } function buildPostEntries(posts: SitemapPost[]) { + // convert each included post into one or more sitemap entries. + // + // a single wordpress post can produce multiple urls if it belongs to both + // technology and community. return posts.flatMap((post) => post.routes.map((route) => ({ + // build the absolute public url using the site base url and the route namespace. url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`, + + // use the wordpress modified time as the sitemap lastmod. lastModified: toIsoDate(post.modified), + changeFrequency: "weekly" as const, + + // give newer posts slightly higher priority to hint at freshness. priority: isRecent(post.modified) ? 0.8 : 0.5, })) ); } function buildAuthorEntries(posts: SitemapPost[]) { + // map author slug -> newest related post modified time. const authorMap = new Map(); for (const post of posts) { const authorName = post.authorName?.trim(); if (!authorName) { + // skip posts with no usable author name. continue; } + // normalize the display name into the frontend author slug format. const authorSlug = sanitizeAuthorSlug(authorName); if (!authorSlug) { continue; @@ -326,11 +456,14 @@ function buildAuthorEntries(posts: SitemapPost[]) { const currentModified = toIsoDate(post.modified); const existingModified = authorMap.get(authorSlug); + // keep the latest related post modification time so the author page lastmod + // reflects the freshest content shown on that author page. if (!existingModified || (currentModified && currentModified > existingModified)) { authorMap.set(authorSlug, currentModified); } } + // convert the author map into final sitemap entries. return Array.from(authorMap.entries()).map(([authorSlug, lastModified]) => ({ url: `${SITE_URL}/authors/${authorSlug}`, lastModified, @@ -340,6 +473,7 @@ function buildAuthorEntries(posts: SitemapPost[]) { } function buildTagEntries(posts: SitemapPost[]) { + // map tag name -> newest related post modified time. const tagMap = new Map(); for (const post of posts) { @@ -348,16 +482,20 @@ function buildTagEntries(posts: SitemapPost[]) { for (const tagName of post.tags) { const normalizedTag = tagName.trim(); if (!normalizedTag) { + // ignore empty or whitespace-only tag names from wordpress. continue; } const existingModified = tagMap.get(normalizedTag); + // keep the latest related post modification time so the tag page lastmod + // tracks the freshest post shown on that tag listing page. if (!existingModified || (postModified && postModified > existingModified)) { tagMap.set(normalizedTag, postModified); } } } + // convert the tag map into final sitemap entries. return Array.from(tagMap.entries()).map(([tagName, lastModified]) => ({ url: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, lastModified, @@ -367,6 +505,10 @@ function buildTagEntries(posts: SitemapPost[]) { } function assertFullSitemap(posts: SitemapPost[]) { + // enforce the "no partial publication" rule. + // + // if wordpress returns an obviously incomplete crawl, fail the refresh so the + // app can fall back to the last successful snapshot instead of publishing bad data. if (!posts.length) { throw new Error("Sitemap generation returned zero posts"); } @@ -382,15 +524,20 @@ function assertFullSitemap(posts: SitemapPost[]) { } export async function getSitemapEntries() { + // crawl all eligible wordpress posts first. const posts = await fetchAllPosts(); + + // do not continue unless the crawl looks complete enough to trust. assertFullSitemap(posts); + // use the newest included post update time as the lastmod for static listing pages. const latestPostModified = getLatestModified(posts) || new Date().toISOString(); const staticEntries = STATIC_ROUTES.map((entry) => ({ ...entry, lastModified: latestPostModified, })); + // combine every sitemap entry type and dedupe by final url. return dedupeEntries([ ...staticEntries, ...buildPostEntries(posts), @@ -400,6 +547,12 @@ export async function getSitemapEntries() { } export function serializeSitemap(entries: SitemapEntry[]) { + // turn the structured entry objects into final sitemap xml. + // + // this stays manual because: + // - pages router does not use app router metadata routes + // - we want full control over xml shape + // - we explicitly do not want xml comments in the output const body = entries .map((entry) => { const parts = [ @@ -420,13 +573,17 @@ export function serializeSitemap(entries: SitemapEntry[]) { async function readPersistedSitemapSnapshot() { try { const xml = await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); + // only trust the fallback file if it still looks like a sitemap document. return isValidSitemapXml(xml) ? xml : null; } catch { + // treat missing or unreadable snapshot files as "no fallback available". return null; } } function isValidSitemapXml(xml: string) { + // perform a lightweight sanity check before serving a persisted snapshot. + // this is not full xml parsing; it only prevents obviously broken files from being used. const normalized = xml.trim(); return ( @@ -438,8 +595,10 @@ function isValidSitemapXml(xml: string) { async function persistSitemapSnapshot(xml: string) { try { + // write the latest good xml to the runtime-local snapshot path. await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); } catch (error) { + // snapshot persistence is helpful but not critical enough to fail the whole refresh. console.error("Failed to persist sitemap snapshot:", error); } } @@ -447,6 +606,12 @@ async function persistSitemapSnapshot(xml: string) { export async function refreshSitemapSnapshot(): Promise { if (!refreshSitemapPromise) { refreshSitemapPromise = (async () => { + // run the full refresh pipeline: + // 1. fetch wordpress content + // 2. validate completeness + // 3. build entries + // 4. serialize xml + // 5. save success as the new fallback snapshot const entries = await getSitemapEntries(); const xml = serializeSitemap(entries); @@ -460,31 +625,41 @@ export async function refreshSitemapSnapshot(): Promise { }; })(); + // once the refresh settles, clear the shared promise so future callers can + // start a new refresh instead of reusing an old completed one. refreshSitemapPromise.finally(() => { refreshSitemapPromise = null; }); } + // if a refresh is already running, every caller waits on the same promise here. return refreshSitemapPromise; } export async function generateSitemapXml() { try { + // first try to generate a fresh full sitemap from wordpress. const result = await refreshSitemapSnapshot(); return result.xml; } catch (error) { - console.error("Fresh sitemap generation failed, trying last successful snapshot:", error); + console.error( + "Fresh sitemap generation failed, trying the last successful snapshot. If snapshot recovery also fails, verify WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL reachability, confirm /tmp is writable for sitemap snapshots, and trigger the sitemap snapshot refresh job.", + error + ); + // first fallback: use the latest successful sitemap held in memory. if (lastSuccessfulSitemapXml) { return lastSuccessfulSitemapXml; } + // second fallback: use the runtime-local snapshot file if it exists and looks valid. const persistedSnapshot = await readPersistedSitemapSnapshot(); if (persistedSnapshot) { lastSuccessfulSitemapXml = persistedSnapshot; return persistedSnapshot; } + // if no fallback exists yet, propagate the error to the caller. throw error; } } diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index 65eae440..ed2dada1 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -9,20 +9,38 @@ export default async function refreshSitemap( req: NextApiRequest, res: NextApiResponse ) { + // read the bearer token supplied by vercel cron or a manual test caller. const authHeader = req.headers.authorization; + + // this shared secret is how we decide whether the caller is allowed to trigger refreshes. const expectedSecret = process.env.CRON_SECRET; + // reject any request that does not provide the expected bearer token. + // + // note: vercel cron automatically includes an "Authorization: Bearer " + // header if the CRON_SECRET environment variable is configured in the project. + // if this consistently returns 401, verify that CRON_SECRET is set in vercel. if (!expectedSecret || authHeader !== `Bearer ${expectedSecret}`) { - return res.status(401).json({ ok: false, message: "Unauthorized" }); + return res.status(401).json({ + ok: false, + message: "Unauthorized - Check CRON_SECRET configuration", + }); } + // limit the endpoint to get requests because vercel cron calls it with get and + // we do not need extra method surface area here. if (req.method !== "GET") { res.setHeader("Allow", "GET"); return res.status(405).json({ ok: false, message: "Method not allowed" }); } try { + // step 1: generate a fresh sitemap snapshot from current wordpress data. + // if this fails, we return 500 and do not attempt google submission. const result = await refreshSitemapSnapshot(); + + // this object reports whether the post-refresh google submission was attempted + // and whether it succeeded, but it does not control the success of the sitemap refresh itself. let searchConsole: | { submitted: boolean; @@ -34,6 +52,8 @@ export default async function refreshSitemap( } | undefined; + // step 2: only after the refresh succeeds, try to submit the sitemap url to + // google search console so google knows to fetch the updated sitemap again. if (isSearchConsoleSubmissionConfigured()) { try { const submission = await submitSitemapToSearchConsole(); @@ -45,6 +65,10 @@ export default async function refreshSitemap( }; } catch (error) { console.error("Google Search Console sitemap submission failed:", error); + + // do not fail the cron request here. + // the sitemap refresh already succeeded, and google submission should remain + // an optional follow-up step rather than a blocker. searchConsole = { submitted: false, message: @@ -54,6 +78,7 @@ export default async function refreshSitemap( }; } } else { + // skip the google step entirely if the required env vars are not configured. searchConsole = { submitted: false, skipped: true, @@ -62,14 +87,23 @@ export default async function refreshSitemap( } return res.status(200).json({ + // this means the sitemap refresh itself succeeded. ok: true, + + // how many final urls were generated into the refreshed sitemap. entryCount: result.entryCount, + + // when the refreshed sitemap snapshot was generated. generatedAt: result.generatedAt, + + // nested status for the optional google submission step. searchConsole, }); } catch (error) { console.error("Scheduled sitemap refresh failed:", error); + // only core refresh failures should produce a 500 here. + // google submission failures are handled above and should not reach this block. return res.status(500).json({ ok: false, message: "Sitemap refresh failed", diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 557cd0d0..32d79d0d 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -2,18 +2,29 @@ import { GetServerSideProps } from "next"; import { generateSitemapXml } from "../lib/sitemap"; function SitemapXml() { + // pages router still expects a component export even though we end the response manually. return null; } export const getServerSideProps: GetServerSideProps = async ({ res }) => { + // generate a fresh sitemap xml if possible, or fall back to the latest successful snapshot. const sitemap = await generateSitemapXml(); + // tell clients and edge caches this response is xml, not html or json. res.setHeader("Content-Type", "application/xml"); + + // cache at the edge for one day, and allow stale responses while revalidation happens. + // this improves crawler latency while keeping the route dynamic. res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=86400"); + + // write the raw xml directly into the response body. res.write(sitemap); + + // finish the response manually because we are not rendering a normal page. res.end(); return { + // next.js still expects a props object from getServerSideProps. props: {}, }; }; From 9bcfa45d7f5ee50aa13ca0e4d8ecf9d824a39855 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Tue, 7 Apr 2026 09:41:12 +0530 Subject: [PATCH 08/28] chore: fix indentation in refresh-sitemap.ts --- pages/api/cron/refresh-sitemap.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index ed2dada1..f9fa2f96 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -43,13 +43,13 @@ export default async function refreshSitemap( // and whether it succeeded, but it does not control the success of the sitemap refresh itself. let searchConsole: | { - submitted: boolean; - submittedAt?: string; - sitemapUrl?: string; - siteUrl?: string; - skipped?: boolean; - message?: string; - } + submitted: boolean; + submittedAt?: string; + sitemapUrl?: string; + siteUrl?: string; + skipped?: boolean; + message?: string; + } | undefined; // step 2: only after the refresh succeeds, try to submit the sitemap url to From 6e573f06dc43f7f7c84bbd383f7c00aadd40017b Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Tue, 7 Apr 2026 09:42:18 +0530 Subject: [PATCH 09/28] chore: sync robots.txt and sitemap.xml from main --- public/robots.txt | 2 +- public/sitemap.xml | 1571 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1572 insertions(+), 1 deletion(-) create mode 100644 public/sitemap.xml diff --git a/public/robots.txt b/public/robots.txt index 27dd535f..54ad43cd 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -49,4 +49,4 @@ Disallow: /blog.keploy.io/ Disallow: /telemetry.keploy.io/ Disallow: /student.keploy.io/ Disallow: /wp/ -Sitemap: https://keploy.io/blog/sitemap.xml +Sitemap: https://keploy.io/blog/sitemap.xml \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 00000000..45fa21f6 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,1571 @@ + + + + + + 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 From adf71841b918bebfef9dc39cdb6a1b1739297dd1 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Tue, 7 Apr 2026 10:11:54 +0530 Subject: [PATCH 10/28] chore: remove sitemap.xml to avoid conflicts with dynamically updated sitemap Signed-off-by: amaan-bhati --- public/sitemap.xml | 1571 ------------------ scripts/submit-sitemap-to-search-console.mjs | 160 ++ 2 files changed, 160 insertions(+), 1571 deletions(-) delete mode 100644 public/sitemap.xml create mode 100644 scripts/submit-sitemap-to-search-console.mjs 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); +} + From aee19611e039047cbf05842524066a76fae0c75c Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 8 Apr 2026 11:53:53 +0530 Subject: [PATCH 11/28] feat: fix and address copilot review#4 Signed-off-by: amaan-bhati --- lib/google-search-console.ts | 10 ++++++++-- lib/sitemap.ts | 2 +- pages/api/cron/refresh-sitemap.ts | 14 +++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/google-search-console.ts b/lib/google-search-console.ts index 1f163efa..315555ac 100644 --- a/lib/google-search-console.ts +++ b/lib/google-search-console.ts @@ -74,7 +74,12 @@ async function fetchGoogleAccessToken() { }); if (!response.ok) { - throw new Error(`Google OAuth token request failed: ${response.status} ${response.statusText}`); + 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 { @@ -125,7 +130,8 @@ export async function submitSitemapToSearchConsole() { if (!response.ok) { const errorBody = await response.text().catch(() => ""); throw new Error( - `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : "" + `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${ + errorBody ? ` - ${errorBody}` : "" }` ); } diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 72e9ec96..959dd253 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -282,7 +282,7 @@ async function fetchAllPosts() { modified ppmaAuthorName tags { - edges {     + edges { node { name } diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index f9fa2f96..eba61a65 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -5,6 +5,14 @@ import { } from "../../../lib/google-search-console"; import { refreshSitemapSnapshot } from "../../../lib/sitemap"; +export const config = { + maxDuration: 300, +}; + +const GOOGLE_SUBMISSION_HELP = + "Verify GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY, " + + "GOOGLE_SEARCH_CONSOLE_SITE_URL, and Search Console property access for the service account."; + export default async function refreshSitemap( req: NextApiRequest, res: NextApiResponse @@ -64,7 +72,7 @@ export default async function refreshSitemap( siteUrl: submission.siteUrl, }; } catch (error) { - console.error("Google Search Console sitemap submission failed:", error); + console.error("Google Search Console sitemap submission failed:", error, GOOGLE_SUBMISSION_HELP); // do not fail the cron request here. // the sitemap refresh already succeeded, and google submission should remain @@ -73,8 +81,8 @@ export default async function refreshSitemap( submitted: false, message: error instanceof Error - ? error.message - : "Google Search Console sitemap submission failed", + ? `${error.message} ${GOOGLE_SUBMISSION_HELP}` + : `Google Search Console sitemap submission failed. ${GOOGLE_SUBMISSION_HELP}`, }; } } else { From 17e675cc2d1ae683eda4cb7a076b21625cc3a33d Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 8 Apr 2026 21:06:28 +0530 Subject: [PATCH 12/28] feat: address copilot review#4 Signed-off-by: amaan-bhati --- lib/sitemap.ts | 41 +++++++++++++++++++++++++++---- pages/api/cron/refresh-sitemap.ts | 5 +++- pages/sitemap.xml.ts | 6 ++--- tests/e2e/Sitemap.spec.ts | 19 ++++++++++++++ tests/mock-server.js | 18 ++++++++++++++ 5 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/Sitemap.spec.ts diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 959dd253..d8a69e4f 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -153,6 +153,33 @@ function isRetryableStatus(status: number) { return [408, 429, 500, 502, 503, 504].includes(status); } +class RetryableFetchError extends Error { + constructor(message: string) { + super(message); + this.name = "RetryableFetchError"; + } +} + +function isRetryableRequestError(error: unknown) { + if (error instanceof RetryableFetchError) { + return true; + } + + if (error instanceof Error) { + // fetch timeout via AbortSignal.timeout commonly surfaces as AbortError/TimeoutError. + if (error.name === "AbortError" || error.name === "TimeoutError") { + return true; + } + + // fetch network failures in node commonly surface as TypeError("fetch failed"). + if (error instanceof TypeError) { + return true; + } + } + + return false; +} + async function fetchGraphQL(query: string, variables: Record = {}) { // remember the last seen error so the final thrown error is meaningful. let lastError: Error | null = null; @@ -179,8 +206,9 @@ async function fetchGraphQL(query: string, variables: Record if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { // back off before retrying so wordpress has a chance to recover and so // we do not hammer the upstream on repeated transient failures. - await sleep(FETCH_RETRY_DELAY_MS * attempt); - continue; + throw new RetryableFetchError( + `WordPress GraphQL request failed with retryable status: ${response.status} ${response.statusText}` + ); } // for non-retryable failures, or when retries are exhausted, fail immediately. @@ -207,8 +235,8 @@ async function fetchGraphQL(query: string, variables: Record // normalize unknown thrown values into an Error instance. lastError = error instanceof Error ? error : new Error(String(error)); - if (attempt < FETCH_RETRY_LIMIT) { - // retry fetch/network/timeout errors too, not just bad http statuses. + if (attempt < FETCH_RETRY_LIMIT && isRetryableRequestError(error)) { + // retry only transient failures (network/timeout/retryable upstream statuses). await sleep(FETCH_RETRY_DELAY_MS * attempt); continue; } @@ -599,7 +627,10 @@ async function persistSitemapSnapshot(xml: string) { await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); } catch (error) { // snapshot persistence is helpful but not critical enough to fail the whole refresh. - console.error("Failed to persist sitemap snapshot:", error); + console.error( + `Failed to persist sitemap snapshot at ${SITEMAP_SNAPSHOT_PATH}. Next step: confirm this runtime can write to /tmp and check filesystem permissions and storage limits.`, + error + ); } } diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index eba61a65..14818ce7 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -108,7 +108,10 @@ export default async function refreshSitemap( searchConsole, }); } catch (error) { - console.error("Scheduled sitemap refresh failed:", error); + console.error( + "Scheduled sitemap refresh failed. Next step: verify WordPress GraphQL reachability and WORDPRESS_API_URL, confirm CRON_SECRET is configured correctly, inspect preceding crawl logs, or rerun this endpoint manually:", + error + ); // only core refresh failures should produce a 500 here. // google submission failures are handled above and should not reach this block. diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 32d79d0d..091f09de 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -13,9 +13,9 @@ export const getServerSideProps: GetServerSideProps = async ({ res }) => { // tell clients and edge caches this response is xml, not html or json. res.setHeader("Content-Type", "application/xml"); - // cache at the edge for one day, and allow stale responses while revalidation happens. - // this improves crawler latency while keeping the route dynamic. - res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=86400"); + // force edge revalidation per request so refreshed sitemap content is served quickly, + // while still allowing stale responses during background revalidation. + res.setHeader("Cache-Control", "s-maxage=0, stale-while-revalidate=86400"); // write the raw xml directly into the response body. res.write(sitemap); diff --git a/tests/e2e/Sitemap.spec.ts b/tests/e2e/Sitemap.spec.ts new file mode 100644 index 00000000..44eb1b64 --- /dev/null +++ b/tests/e2e/Sitemap.spec.ts @@ -0,0 +1,19 @@ +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=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/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; } From c69ebc8b4cb85ddf2dd3874f589527ddd0f077b3 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 8 Apr 2026 22:36:26 +0530 Subject: [PATCH 13/28] feat: fix and address copilot reviews#5 Signed-off-by: amaan-bhati --- lib/sitemap.ts | 68 ++++++---------------------- playwright.config.ts | 1 + tests/e2e/RefreshSitemapCron.spec.ts | 51 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 tests/e2e/RefreshSitemapCron.spec.ts diff --git a/lib/sitemap.ts b/lib/sitemap.ts index d8a69e4f..fa611da2 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -107,15 +107,6 @@ type AllPostsQueryResponse = { }; }; -export type PostCategoryConnection = { - edges?: Array<{ - node?: { - name?: string | null; - slug?: string | null; - } | null; - } | null> | null; -} | null; - 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 }, @@ -153,33 +144,6 @@ function isRetryableStatus(status: number) { return [408, 429, 500, 502, 503, 504].includes(status); } -class RetryableFetchError extends Error { - constructor(message: string) { - super(message); - this.name = "RetryableFetchError"; - } -} - -function isRetryableRequestError(error: unknown) { - if (error instanceof RetryableFetchError) { - return true; - } - - if (error instanceof Error) { - // fetch timeout via AbortSignal.timeout commonly surfaces as AbortError/TimeoutError. - if (error.name === "AbortError" || error.name === "TimeoutError") { - return true; - } - - // fetch network failures in node commonly surface as TypeError("fetch failed"). - if (error instanceof TypeError) { - return true; - } - } - - return false; -} - async function fetchGraphQL(query: string, variables: Record = {}) { // remember the last seen error so the final thrown error is meaningful. let lastError: Error | null = null; @@ -206,9 +170,8 @@ async function fetchGraphQL(query: string, variables: Record if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { // back off before retrying so wordpress has a chance to recover and so // we do not hammer the upstream on repeated transient failures. - throw new RetryableFetchError( - `WordPress GraphQL request failed with retryable status: ${response.status} ${response.statusText}` - ); + await sleep(FETCH_RETRY_DELAY_MS * attempt); + continue; } // for non-retryable failures, or when retries are exhausted, fail immediately. @@ -235,8 +198,8 @@ async function fetchGraphQL(query: string, variables: Record // normalize unknown thrown values into an Error instance. lastError = error instanceof Error ? error : new Error(String(error)); - if (attempt < FETCH_RETRY_LIMIT && isRetryableRequestError(error)) { - // retry only transient failures (network/timeout/retryable upstream statuses). + if (attempt < FETCH_RETRY_LIMIT) { + // retry fetch/network/timeout errors too, not just bad http statuses. await sleep(FETCH_RETRY_DELAY_MS * attempt); continue; } @@ -246,7 +209,9 @@ async function fetchGraphQL(query: string, variables: Record throw lastError || new Error("WordPress GraphQL request failed"); } -function mapCategoriesToRoutes(categories?: PostCategoryConnection) { +function mapCategoriesToRoutes( + categories?: AllPostsQueryResponse["posts"]["edges"][number]["node"]["categories"] +) { // use a set so one post cannot produce duplicate routes even if wordpress returns // the same category in multiple forms. const routes = new Set(); @@ -388,7 +353,6 @@ function toIsoDate(value?: string) { } // convert the wordpress date string into a normalized iso string. - // // why this exists: // - wordpress may return a parseable date string format // - the sitemap should emit a consistent machine-readable timestamp @@ -426,7 +390,7 @@ function dedupeEntries(entries: SitemapEntry[]) { function getLatestModified(posts: SitemapPost[]) { // find the newest modified timestamp across all included posts. - // + // this value is later used for high-level listing pages like /blog, /technology, // and /community so those pages appear updated when the newest underlying post changes. return posts.reduce((latest, post) => { @@ -445,7 +409,7 @@ function getLatestModified(posts: SitemapPost[]) { function buildPostEntries(posts: SitemapPost[]) { // convert each included post into one or more sitemap entries. - // + // a single wordpress post can produce multiple urls if it belongs to both // technology and community. return posts.flatMap((post) => @@ -534,7 +498,7 @@ function buildTagEntries(posts: SitemapPost[]) { function assertFullSitemap(posts: SitemapPost[]) { // enforce the "no partial publication" rule. - // + // if wordpress returns an obviously incomplete crawl, fail the refresh so the // app can fall back to the last successful snapshot instead of publishing bad data. if (!posts.length) { @@ -576,7 +540,7 @@ export async function getSitemapEntries() { export function serializeSitemap(entries: SitemapEntry[]) { // turn the structured entry objects into final sitemap xml. - // + // this stays manual because: // - pages router does not use app router metadata routes // - we want full control over xml shape @@ -627,10 +591,7 @@ async function persistSitemapSnapshot(xml: string) { await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); } catch (error) { // snapshot persistence is helpful but not critical enough to fail the whole refresh. - console.error( - `Failed to persist sitemap snapshot at ${SITEMAP_SNAPSHOT_PATH}. Next step: confirm this runtime can write to /tmp and check filesystem permissions and storage limits.`, - error - ); + console.error("Failed to persist sitemap snapshot:", error); } } @@ -673,10 +634,7 @@ export async function generateSitemapXml() { const result = await refreshSitemapSnapshot(); return result.xml; } catch (error) { - console.error( - "Fresh sitemap generation failed, trying the last successful snapshot. If snapshot recovery also fails, verify WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL reachability, confirm /tmp is writable for sitemap snapshots, and trigger the sitemap snapshot refresh job.", - error - ); + console.error("Fresh sitemap generation failed, trying last successful snapshot:", error); // first fallback: use the latest successful sitemap held in memory. if (lastSuccessfulSitemapXml) { diff --git a/playwright.config.ts b/playwright.config.ts index 237f3b15..c38a6a8c 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: process.env.CRON_SECRET || 'test-secret', }, }, ], 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'); + }); +}); From 69905c26a87649dd9de5317ae7c47c46b336f023 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Wed, 8 Apr 2026 23:01:21 +0530 Subject: [PATCH 14/28] feat: fix and address copilot review#6 Signed-off-by: amaan-bhati --- lib/sitemap.ts | 47 +++++++++++++++++++++++++++++++++++++---------- vercel.json | 9 +++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index fa611da2..2a640a91 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -144,6 +144,31 @@ function isRetryableStatus(status: number) { return [408, 429, 500, 502, 503, 504].includes(status); } +class RetryableFetchError extends Error { + constructor(message: string) { + super(message); + this.name = "RetryableFetchError"; + } +} + +function isRetryableRequestError(error: unknown) { + if (error instanceof RetryableFetchError) { + return true; + } + + if (error instanceof Error) { + if (error.name === "AbortError" || error.name === "TimeoutError") { + return true; + } + + if (error instanceof TypeError) { + return true; + } + } + + return false; +} + async function fetchGraphQL(query: string, variables: Record = {}) { // remember the last seen error so the final thrown error is meaningful. let lastError: Error | null = null; @@ -168,10 +193,9 @@ async function fetchGraphQL(query: string, variables: Record if (!response.ok) { if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { - // back off before retrying so wordpress has a chance to recover and so - // we do not hammer the upstream on repeated transient failures. - await sleep(FETCH_RETRY_DELAY_MS * attempt); - continue; + throw new RetryableFetchError( + `WordPress GraphQL request failed with retryable status: ${response.status} ${response.statusText}` + ); } // for non-retryable failures, or when retries are exhausted, fail immediately. @@ -198,11 +222,14 @@ async function fetchGraphQL(query: string, variables: Record // normalize unknown thrown values into an Error instance. lastError = error instanceof Error ? error : new Error(String(error)); - if (attempt < FETCH_RETRY_LIMIT) { - // retry fetch/network/timeout errors too, not just bad http statuses. + if (attempt < FETCH_RETRY_LIMIT && isRetryableRequestError(error)) { + // retry only transient failures (network/timeout/retryable upstream statuses). await sleep(FETCH_RETRY_DELAY_MS * attempt); continue; } + + // fail fast for non-retryable errors and once retry attempts are exhausted. + throw lastError; } } @@ -390,7 +417,6 @@ function dedupeEntries(entries: SitemapEntry[]) { function getLatestModified(posts: SitemapPost[]) { // find the newest modified timestamp across all included posts. - // this value is later used for high-level listing pages like /blog, /technology, // and /community so those pages appear updated when the newest underlying post changes. return posts.reduce((latest, post) => { @@ -409,7 +435,6 @@ function getLatestModified(posts: SitemapPost[]) { function buildPostEntries(posts: SitemapPost[]) { // convert each included post into one or more sitemap entries. - // a single wordpress post can produce multiple urls if it belongs to both // technology and community. return posts.flatMap((post) => @@ -498,7 +523,6 @@ function buildTagEntries(posts: SitemapPost[]) { function assertFullSitemap(posts: SitemapPost[]) { // enforce the "no partial publication" rule. - // if wordpress returns an obviously incomplete crawl, fail the refresh so the // app can fall back to the last successful snapshot instead of publishing bad data. if (!posts.length) { @@ -591,7 +615,10 @@ async function persistSitemapSnapshot(xml: string) { await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); } catch (error) { // snapshot persistence is helpful but not critical enough to fail the whole refresh. - console.error("Failed to persist sitemap snapshot:", error); + console.error( + `Failed to persist sitemap snapshot at ${SITEMAP_SNAPSHOT_PATH}. Check that the runtime allows writes to the temp directory and that sufficient permissions and storage are available:`, + error + ); } } diff --git a/vercel.json b/vercel.json index b129ca2b..8d4cc87d 100644 --- a/vercel.json +++ b/vercel.json @@ -46,6 +46,15 @@ } ] }, + { + "source": "/blog/sitemap.xml", + "headers": [ + { + "key": "Cache-Control", + "value": "s-maxage=0, stale-while-revalidate=86400" + } + ] + }, { "source": "/blog/(.*)", "headers": [ From 6064ea073d4210feaf652c35dc6b58df15c1075e Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 00:12:17 +0530 Subject: [PATCH 15/28] feat: address copilot comment#7 Signed-off-by: amaan-bhati --- pages/sitemap.xml.ts | 6 +++--- tests/e2e/Sitemap.spec.ts | 3 ++- vercel.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 091f09de..36b6f261 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -13,9 +13,9 @@ export const getServerSideProps: GetServerSideProps = async ({ res }) => { // tell clients and edge caches this response is xml, not html or json. res.setHeader("Content-Type", "application/xml"); - // force edge revalidation per request so refreshed sitemap content is served quickly, - // while still allowing stale responses during background revalidation. - res.setHeader("Cache-Control", "s-maxage=0, stale-while-revalidate=86400"); + // cache at the edge for one day to reduce request-time load, while allowing stale + // responses during background revalidation. + res.setHeader("Cache-Control", "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400"); // write the raw xml directly into the response body. res.write(sitemap); diff --git a/tests/e2e/Sitemap.spec.ts b/tests/e2e/Sitemap.spec.ts index 44eb1b64..7757a56c 100644 --- a/tests/e2e/Sitemap.spec.ts +++ b/tests/e2e/Sitemap.spec.ts @@ -6,7 +6,8 @@ test.describe('Sitemap Route', () => { expect(response.status()).toBe(200); expect(response.headers()['content-type']).toContain('application/xml'); - expect(response.headers()['cache-control']).toContain('s-maxage=0'); + 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(); diff --git a/vercel.json b/vercel.json index 8d4cc87d..ce011b9a 100644 --- a/vercel.json +++ b/vercel.json @@ -51,7 +51,7 @@ "headers": [ { "key": "Cache-Control", - "value": "s-maxage=0, stale-while-revalidate=86400" + "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" } ] }, From f1b26457ec69f3929326415b914967f12d447a60 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 03:42:27 +0530 Subject: [PATCH 16/28] feat: fix copilot review#8, fix bugs caught by copilot and claude code Signed-off-by: amaan-bhati --- lib/sitemap.ts | 12 +++++++++--- vercel.json | 16 ++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 2a640a91..b858cf38 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -2,6 +2,7 @@ import { promises as fs } from "fs"; import path from "path"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; +import { sanitizeStringForURL } from "../utils/sanitizeStringForUrl"; // choose the wordpress graphql endpoint in this order: // server-only env for production or local server use @@ -514,13 +515,18 @@ function buildTagEntries(posts: SitemapPost[]) { // convert the tag map into final sitemap entries. return Array.from(tagMap.entries()).map(([tagName, lastModified]) => ({ - url: `${SITE_URL}/tag/${encodeURIComponent(tagName)}`, + url: `${SITE_URL}/tag/${sanitizeStringForURL(tagName)}`, lastModified, changeFrequency: "weekly" as const, priority: 0.7, })); } +// minimum number of posts required per category before the sitemap is considered +// trustworthy enough to publish. this prevents a severely degraded wordpress +// response (e.g. first page only, partial crawl) from replacing a good snapshot. +const MIN_POSTS_PER_CATEGORY = 5; + function assertFullSitemap(posts: SitemapPost[]) { // enforce the "no partial publication" rule. // if wordpress returns an obviously incomplete crawl, fail the refresh so the @@ -532,9 +538,9 @@ function assertFullSitemap(posts: SitemapPost[]) { const technologyCount = posts.filter((post) => post.routes.includes("technology")).length; const communityCount = posts.filter((post) => post.routes.includes("community")).length; - if (!technologyCount || !communityCount) { + if (technologyCount < MIN_POSTS_PER_CATEGORY || communityCount < MIN_POSTS_PER_CATEGORY) { throw new Error( - `Sitemap generation incomplete: technology=${technologyCount}, community=${communityCount}` + `Sitemap generation incomplete: technology=${technologyCount}, community=${communityCount} (minimum ${MIN_POSTS_PER_CATEGORY} required per category)` ); } } diff --git a/vercel.json b/vercel.json index ce011b9a..f888540e 100644 --- a/vercel.json +++ b/vercel.json @@ -47,24 +47,24 @@ ] }, { - "source": "/blog/sitemap.xml", + "source": "/blog/(.*)", "headers": [ + { + "key": "Content-Security-Policy", + "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" + }, { "key": "Cache-Control", - "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" + "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" } ] }, { - "source": "/blog/(.*)", + "source": "/blog/sitemap.xml", "headers": [ - { - "key": "Content-Security-Policy", - "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" - }, { "key": "Cache-Control", - "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" + "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" } ] } From ea815709d95ede7c77c165d6269725a3c6f98bc8 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 03:58:59 +0530 Subject: [PATCH 17/28] feat: fix copilot review#9, fix the /tmp folder edge case Signed-off-by: amaan-bhati --- lib/sitemap.ts | 8 ++- playwright.config.ts | 2 +- tests/fixtures/community-posts.json | 84 ++++++++++++++++++++++++++++ tests/fixtures/technology-posts.json | 42 ++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index b858cf38..3327dbd1 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -1,4 +1,5 @@ import { promises as fs } from "fs"; +import os from "os"; import path from "path"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; @@ -18,7 +19,7 @@ const WP_API_URL = // store the last successful xml in /tmp so a later failed refresh can fall back to it // this is runtime local storage, not durable database storage, so it helps during the // life of a runtime instance but is not guaranteed across instance replacement -const SITEMAP_SNAPSHOT_PATH = path.join("/tmp", "keploy-blog-sitemap.xml"); +const SITEMAP_SNAPSHOT_PATH = path.join(os.tmpdir(), "keploy-blog-sitemap.xml"); // how many times a single wordpress request can be retried before failing. const FETCH_RETRY_LIMIT = 6; @@ -667,7 +668,10 @@ export async function generateSitemapXml() { const result = await refreshSitemapSnapshot(); return result.xml; } catch (error) { - console.error("Fresh sitemap generation failed, trying last successful snapshot:", error); + console.error( + `Fresh sitemap generation failed; attempting the last successful snapshot. Next steps: verify WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL is configured correctly and reachable at ${WP_API_URL}, then retry the sitemap refresh once WordPress GraphQL connectivity is restored.`, + error, + ); // first fallback: use the latest successful sitemap held in memory. if (lastSuccessfulSitemapXml) { diff --git a/playwright.config.ts b/playwright.config.ts index c38a6a8c..984b33fc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -116,7 +116,7 @@ export default defineConfig({ env: { WORDPRESS_API_URL: GRAPHQL_API_URL, NEXT_PUBLIC_WORDPRESS_API_URL: GRAPHQL_API_URL, - CRON_SECRET: process.env.CRON_SECRET || 'test-secret', + CRON_SECRET: 'test-secret', }, }, ], 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, From 1649fd61020f2c5e7e0c1c53680cf5b5c14d05d7 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 04:13:47 +0530 Subject: [PATCH 18/28] feat: fix copilot review#10, fix typescript error, empty tagdlug guard Signed-off-by: amaan-bhati --- lib/sitemap.ts | 26 +++++++++++++++++--------- vercel.json | 16 ++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 3327dbd1..692f53ea 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -109,6 +109,10 @@ type AllPostsQueryResponse = { }; }; +type PostNode = NonNullable< + NonNullable["edges"]>[number]["node"] +>; + 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 }, @@ -238,9 +242,7 @@ async function fetchGraphQL(query: string, variables: Record throw lastError || new Error("WordPress GraphQL request failed"); } -function mapCategoriesToRoutes( - categories?: AllPostsQueryResponse["posts"]["edges"][number]["node"]["categories"] -) { +function mapCategoriesToRoutes(categories?: PostNode["categories"]) { // use a set so one post cannot produce duplicate routes even if wordpress returns // the same category in multiple forms. const routes = new Set(); @@ -515,12 +517,18 @@ function buildTagEntries(posts: SitemapPost[]) { } // convert the tag map into final sitemap entries. - return Array.from(tagMap.entries()).map(([tagName, lastModified]) => ({ - url: `${SITE_URL}/tag/${sanitizeStringForURL(tagName)}`, - lastModified, - changeFrequency: "weekly" as const, - priority: 0.7, - })); + 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, + }]; + }); } // minimum number of posts required per category before the sitemap is considered diff --git a/vercel.json b/vercel.json index f888540e..ce011b9a 100644 --- a/vercel.json +++ b/vercel.json @@ -47,24 +47,24 @@ ] }, { - "source": "/blog/(.*)", + "source": "/blog/sitemap.xml", "headers": [ - { - "key": "Content-Security-Policy", - "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" - }, { "key": "Cache-Control", - "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" + "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" } ] }, { - "source": "/blog/sitemap.xml", + "source": "/blog/(.*)", "headers": [ + { + "key": "Content-Security-Policy", + "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" + }, { "key": "Cache-Control", - "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" + "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" } ] } From 0bd1bfd5c1088925d422b09344ed8eeb55d82866 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 04:35:22 +0530 Subject: [PATCH 19/28] feat: fix copilot review#10, vercel cache control Signed-off-by: amaan-bhati --- lib/sitemap.ts | 11 ++++++++++- vercel.json | 16 ++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 692f53ea..02f48033 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -214,7 +214,16 @@ async function fetchGraphQL(query: string, variables: Record if (json.errors?.length) { // graphql can return application-level errors even when the http response is 200. const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); - throw new Error(message || "WordPress GraphQL returned errors"); + const graphqlErrorMessage = message || "WordPress GraphQL returned errors"; + + // treat graphql-level errors as retryable while attempts remain because + // wpgraphql can return transient errors (e.g. during plugin reload or db lock) + // with a 200 http response. failing immediately on first occurrence would miss + // these cases that the retry loop is specifically designed to handle. + if (attempt < FETCH_RETRY_LIMIT) { + throw new RetryableFetchError(graphqlErrorMessage); + } + throw new Error(graphqlErrorMessage); } if (!json.data) { diff --git a/vercel.json b/vercel.json index ce011b9a..2c6dfb04 100644 --- a/vercel.json +++ b/vercel.json @@ -47,24 +47,24 @@ ] }, { - "source": "/blog/sitemap.xml", + "source": "/blog/((?!sitemap\\.xml$).*)", "headers": [ + { + "key": "Content-Security-Policy", + "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" + }, { "key": "Cache-Control", - "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" + "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" } ] }, { - "source": "/blog/(.*)", + "source": "/blog/sitemap.xml", "headers": [ - { - "key": "Content-Security-Policy", - "value": "connect-src 'self' https://px.ads.linkedin.com https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://rp.liadm.com https://idx.liadm.com https://pagead2.googlesyndication.com https://*.clarity.ms https://news.google.com https://assets.apollo.io https://wp.keploy.io https://cdn.hashnode.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://*.googlevideo.com https://googleads.g.doubleclick.net https://marketplace.visualstudio.com https://api.github.com https://pro.ip-api.com https://api.vector.co https://aplo-evnt.com https://ep1.adtrafficquality.google https://ppptg.com https://telemetry.keploy.io; frame-src 'self' https://www.googletagmanager.com https://keploy-websites.vercel.app https://blog-website-phi-eight.vercel.app https://docbot.keploy.io https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://news.google.com https://googleads.g.doubleclick.net https://*.google.com https://ppptg.com; img-src 'self' https://c.bing.com https://ppptg.com https://pbs.twimg.com https://secure.gravatar.com https://wp.keploy.io https://keploy.io data:;" - }, { "key": "Cache-Control", - "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" + "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" } ] } From 13390be3d7a6b7ae1dab728ba802768d49b6a8bd Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 04:45:31 +0530 Subject: [PATCH 20/28] feat: fix copilot review#11, fallback sitemapxml inclusion Signed-off-by: amaan-bhati --- lib/sitemap.ts | 12 ++++++++++++ pages/sitemap.xml.ts | 25 ++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 02f48033..8293de4a 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -610,6 +610,18 @@ export function serializeSitemap(entries: SitemapEntry[]) { `${body}`; } +export function getStaticFallbackXml() { + // generate a minimal sitemap from the hardcoded static routes only. + // this requires no wordpress data and is used as a last-resort response + // when all three generation tiers fail (fresh, in-memory, and /tmp snapshot). + // it ensures crawlers always receive valid xml at /sitemap.xml rather than + // an html error page from next.js. + const now = new Date().toISOString(); + return serializeSitemap( + STATIC_ROUTES.map((route) => ({ ...route, lastModified: now })) + ); +} + async function readPersistedSitemapSnapshot() { try { const xml = await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 36b6f261..17667cd1 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -1,5 +1,5 @@ import { GetServerSideProps } from "next"; -import { generateSitemapXml } from "../lib/sitemap"; +import { generateSitemapXml, getStaticFallbackXml } from "../lib/sitemap"; function SitemapXml() { // pages router still expects a component export even though we end the response manually. @@ -7,8 +7,25 @@ function SitemapXml() { } export const getServerSideProps: GetServerSideProps = async ({ res }) => { - // generate a fresh sitemap xml if possible, or fall back to the latest successful snapshot. - const sitemap = await generateSitemapXml(); + let sitemap: string; + let statusCode = 200; + + try { + // generate a fresh sitemap xml if possible, or fall back to the latest successful snapshot. + sitemap = await generateSitemapXml(); + } catch (error) { + // all three generation tiers failed (fresh, in-memory snapshot, /tmp snapshot). + // this only happens on a cold start when wordpress is completely unreachable. + // serve the static-only fallback so crawlers always receive valid xml here + // instead of next.js's default html error page. + console.error( + "Sitemap generation failed with no snapshot available; serving static-routes-only fallback. " + + "Verify WordPress reachability and run the cron refresh endpoint once connectivity is restored.", + error, + ); + sitemap = getStaticFallbackXml(); + statusCode = 503; + } // tell clients and edge caches this response is xml, not html or json. res.setHeader("Content-Type", "application/xml"); @@ -17,6 +34,8 @@ export const getServerSideProps: GetServerSideProps = async ({ res }) => { // responses during background revalidation. res.setHeader("Cache-Control", "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400"); + res.statusCode = statusCode; + // write the raw xml directly into the response body. res.write(sitemap); From 14f78b496a67b26f39201aa17e50b58314119782 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 04:54:10 +0530 Subject: [PATCH 21/28] feat: fix copilot review#12, cronsecret missconfigureation and cache control on 503 Signed-off-by: amaan-bhati --- pages/api/cron/refresh-sitemap.ts | 22 +++++++++++++++++----- pages/sitemap.xml.ts | 10 +++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index 14818ce7..643685da 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -23,15 +23,27 @@ export default async function refreshSitemap( // this shared secret is how we decide whether the caller is allowed to trigger refreshes. const expectedSecret = process.env.CRON_SECRET; - // reject any request that does not provide the expected bearer token. + // fail with 500 if the deployment is missing CRON_SECRET entirely — this is a + // server misconfiguration, not a client auth problem, and should be diagnosed + // separately from a caller sending the wrong token. + if (!expectedSecret) { + console.error( + "CRON_SECRET is not configured. Next step: set CRON_SECRET in the Vercel environment variables, redeploy if required, then retry." + ); + return res.status(500).json({ + ok: false, + message: "Server misconfiguration - CRON_SECRET is not configured", + }); + } + + // reject callers that do not provide the expected bearer token. // // note: vercel cron automatically includes an "Authorization: Bearer " - // header if the CRON_SECRET environment variable is configured in the project. - // if this consistently returns 401, verify that CRON_SECRET is set in vercel. - if (!expectedSecret || authHeader !== `Bearer ${expectedSecret}`) { + // header when CRON_SECRET is configured in the project settings. + if (authHeader !== `Bearer ${expectedSecret}`) { return res.status(401).json({ ok: false, - message: "Unauthorized - Check CRON_SECRET configuration", + message: "Unauthorized", }); } diff --git a/pages/sitemap.xml.ts b/pages/sitemap.xml.ts index 17667cd1..b1950d8f 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -30,9 +30,13 @@ export const getServerSideProps: GetServerSideProps = async ({ res }) => { // tell clients and edge caches this response is xml, not html or json. res.setHeader("Content-Type", "application/xml"); - // cache at the edge for one day to reduce request-time load, while allowing stale - // responses during background revalidation. - res.setHeader("Cache-Control", "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400"); + // only cache successful sitemaps at the edge for one day. + // do not cache degraded fallback responses so crawlers recover quickly once wordpress is back. + if (statusCode >= 500) { + res.setHeader("Cache-Control", "no-store"); + } else { + res.setHeader("Cache-Control", "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400"); + } res.statusCode = statusCode; From 9c28bf2dcadae15587b5b1d196799d6eaf7c5c71 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 12:30:47 +0530 Subject: [PATCH 22/28] feat: fix copilot review#13, improve 503 path, apply csp to route paths, change maxduration to 60 Signed-off-by: amaan-bhati --- next.config.js | 4 +++- pages/sitemap.xml.ts | 7 +++++++ vercel.json | 9 --------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/next.config.js b/next.config.js index 0bf7e7b2..08ef8819 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/sitemap.xml.ts b/pages/sitemap.xml.ts index b1950d8f..879d93bc 100644 --- a/pages/sitemap.xml.ts +++ b/pages/sitemap.xml.ts @@ -1,6 +1,13 @@ import { GetServerSideProps } from "next"; import { generateSitemapXml, getStaticFallbackXml } from "../lib/sitemap"; +// give this route enough time to complete a fresh wordpress crawl before timing out. +// the cron handler has its own 300s budget; 60s is sufficient for user-facing requests +// because the retry budget is smaller and the typical crawl completes in under 20s. +export const config = { + maxDuration: 60, +}; + function SitemapXml() { // pages router still expects a component export even though we end the response manually. return null; diff --git a/vercel.json b/vercel.json index 2c6dfb04..e5ce7780 100644 --- a/vercel.json +++ b/vercel.json @@ -58,15 +58,6 @@ "value": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800" } ] - }, - { - "source": "/blog/sitemap.xml", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400" - } - ] } ] } From e25b84805fc8a8c19f0aaa2f3d3841b0b38157b0 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 12:54:36 +0530 Subject: [PATCH 23/28] feat: fix copilot review#14, exclude /api for for grouped negative lookahead Signed-off-by: amaan-bhati --- vercel.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index e5ce7780..359f525e 100644 --- a/vercel.json +++ b/vercel.json @@ -11,6 +11,11 @@ "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", @@ -47,7 +52,7 @@ ] }, { - "source": "/blog/((?!sitemap\\.xml$).*)", + "source": "/blog/((?!(?:sitemap\\.xml$|api/|_next/static/)).*)", "headers": [ { "key": "Content-Security-Policy", From a558228eea697b06535c25a2ab97a160f6959da1 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 13:44:04 +0530 Subject: [PATCH 24/28] chore: copilot minor suggestion address for future people working Signed-off-by: amaan-bhati --- lib/sitemap.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 8293de4a..55fd6c42 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -8,7 +8,9 @@ import { sanitizeStringForURL } from "../utils/sanitizeStringForUrl"; // choose the wordpress graphql endpoint in this order: // server-only env for production or local server use // public env as a fallback if the server env is missing -// hardcoded endpoint so local development still works out of the box +// hardcoded endpoint as a last resort (note: next.config.js throws at startup if +// WORDPRESS_API_URL is absent, so this fallback is only reachable outside the +// normal next.js dev/build lifecycle, e.g. direct ts-node invocation) // this value is the single source used for every wordpress fetch in the sitemap flow. const WP_API_URL = From 3266d6b0057ca2e1962f9804a255d270ce60fde2 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Thu, 9 Apr 2026 14:50:08 +0530 Subject: [PATCH 25/28] chore: fix copilot review#16, cusor pagination fix Signed-off-by: amaan-bhati --- lib/sitemap.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 55fd6c42..65597fef 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -377,7 +377,18 @@ async function fetchAllPosts() { } hasNextPage = Boolean(data.posts?.pageInfo?.hasNextPage); - after = data.posts?.pageInfo?.endCursor || null; + const nextCursor = data.posts?.pageInfo?.endCursor || null; + + // guard against a malformed wordpress response that claims more pages exist but + // returns no cursor to advance to — without this, after stays null and the loop + // re-fetches the first page indefinitely until the function is killed. + if (hasNextPage && !nextCursor) { + throw new Error( + "WordPress pagination returned hasNextPage: true with no endCursor — cannot safely continue" + ); + } + + after = nextCursor; if (hasNextPage) { // insert a small delay before fetching the next page to reduce pressure on wpgraphql. From fca7d506114507d3926d6699177562cf0f68000f Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 17:13:58 +0530 Subject: [PATCH 26/28] feat: migrate to isr generation of sitemap Signed-off-by: amaan-bhati --- app/layout.tsx | 7 + app/sitemap.xml/route.ts | 93 +++++ components/AuthorMapping.tsx | 4 +- components/NotFoundPage.tsx | 12 +- components/TableContents.tsx | 4 +- components/more-stories.tsx | 2 +- components/post-body.tsx | 6 +- lib/api.ts | 41 +- lib/sitemap.ts | 671 +++++------------------------- pages/api/cron/refresh-sitemap.ts | 125 ++---- pages/authors/[slug].tsx | 2 +- pages/community/[slug].tsx | 4 +- pages/sitemap.xml.ts | 62 --- pages/technology/[slug].tsx | 4 +- tsconfig.json | 29 +- 15 files changed, 296 insertions(+), 770 deletions(-) create mode 100644 app/layout.tsx create mode 100644 app/sitemap.xml/route.ts delete mode 100644 pages/sitemap.xml.ts diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..1cbd70f0 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,7 @@ +// Required by Next.js App Router — every project with an app/ directory must have +// a root layout. All real layout (fonts, global CSS, scripts, structured data) +// lives in pages/_app.tsx and pages/_document.tsx, which continue to serve all +// existing pages/ routes unchanged. This layout only applies to routes inside app/. +export default function RootLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/app/sitemap.xml/route.ts b/app/sitemap.xml/route.ts new file mode 100644 index 00000000..4963edb2 --- /dev/null +++ b/app/sitemap.xml/route.ts @@ -0,0 +1,93 @@ +import { getAllPosts } from "../../lib/api"; +import { + adaptPostsForSitemap, + assertFullSitemap, + buildAuthorEntries, + buildPostEntries, + buildTagEntries, + dedupeEntries, + getLatestModified, + getStaticFallbackXml, + serializeSitemap, + STATIC_ROUTES, +} from "../../lib/sitemap"; + +// ISR: Vercel caches this response in its persistent CDN edge cache for 1 hour. + +// What this means in practice: +// After the first generation, every request is served from CDN (<10ms, no Lambda invoked). +// After TTL expires: Vercel immediately serves the stale cached version to the +// requesting client, then regenerates in the background. The client never waits. +// If WordPress is down during background regen: Vercel silently keeps serving the +// previous cached version. No fallback code needed, it is platform behaviour. +// The only case where no cached version exists is the very first request after deploy, +// or if generation has never succeeded (WordPress down on cold start). this can be ignored for a while but we can add +// a fix for this as well, we can explore prewarming, while the build itself, if we add a cronjob + +// without prewarming: User → triggers ISR → waits 13s ❌ +// prewarming: CI/CD → triggers ISR → warms cache ✅ +// User → hits CDN → instant response ✅ + +export const revalidate = 3600; + +export async function GET(): Promise { + try { + // reuse the existing getAllPosts() paginator from lib/api.ts. + // as of the pagination fix, this fetches ALL posts (not just the first 50). + const allPostsResult = await getAllPosts(); + + // convert getAllPosts() return shape into SitemapPost[] for the entry builders. + const posts = adaptPostsForSitemap(allPostsResult); + + // reject partial wordpress responses before they replace a good cached version. + // throws if fewer than 5 posts per category, 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 instructs Vercel's CDN to cache for 1h (matches revalidate above). + // stale-while-revalidate lets the CDN serve stale while regenerating in background. + // max-age=0 ensures browsers always revalidate with the CDN rather than caching locally. + "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. + // Vercel will keep serving the previous good cached version on the CDN. + // 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.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/sitemap.ts b/lib/sitemap.ts index 65597fef..b8446729 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -1,43 +1,7 @@ -import { promises as fs } from "fs"; -import os from "os"; -import path from "path"; import { SITE_URL } from "./structured-data"; import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; import { sanitizeStringForURL } from "../utils/sanitizeStringForUrl"; -// choose the wordpress graphql endpoint in this order: -// server-only env for production or local server use -// public env as a fallback if the server env is missing -// hardcoded endpoint as a last resort (note: next.config.js throws at startup if -// WORDPRESS_API_URL is absent, so this fallback is only reachable outside the -// normal next.js dev/build lifecycle, e.g. direct ts-node invocation) - -// this value is the single source used for every wordpress fetch in the sitemap flow. -const WP_API_URL = - process.env.WORDPRESS_API_URL || - process.env.NEXT_PUBLIC_WORDPRESS_API_URL || - "https://wp.keploy.io/graphql"; - -// store the last successful xml in /tmp so a later failed refresh can fall back to it -// this is runtime local storage, not durable database storage, so it helps during the -// life of a runtime instance but is not guaranteed across instance replacement -const SITEMAP_SNAPSHOT_PATH = path.join(os.tmpdir(), "keploy-blog-sitemap.xml"); - -// how many times a single wordpress request can be retried before failing. -const FETCH_RETRY_LIMIT = 6; - -// base retry delay in milliseconds. the actual wait grows with each attempt. -const FETCH_RETRY_DELAY_MS = 2000; - -// fail a single wordpress request if it hangs too long. -const FETCH_TIMEOUT_MS = 25000; - -// fetch posts in pages of 50 to avoid overloading wordpress with huge payloads. -const POSTS_PAGE_SIZE = 50; - -// wait briefly between pages so the crawl is less aggressive on wpgraphql. -const PAGE_SETTLE_DELAY_MS = 250; - type SitemapChangeFrequency = "daily" | "weekly" | "monthly"; type CategoryRoute = "technology" | "community"; @@ -55,67 +19,17 @@ export type SitemapEntry = { priority?: number; }; -type GraphQLResponse = { - // graphql returns successful payloads under data. - data?: T; - - // graphql can also return logical errors even when the http request itself succeeded. - errors?: Array<{ message?: string }>; -}; - +// internal shape used by all entry builders. +// populated via adaptPostsForSitemap() from the getAllPosts() return value. type SitemapPost = { - // wordpress post slug used to build the frontend url. slug: string; - - // wordpress last updated timestamp for the post. modified?: string; - - // author name returned by wordpress for this post. authorName?: string; - - // tag names attached to the post. tags: string[]; - - // local route namespaces this post belongs to after category mapping. routes: CategoryRoute[]; }; -type AllPostsQueryResponse = { - posts?: { - edges?: Array<{ - node?: { - slug?: string; - modified?: string; - ppmaAuthorName?: string; - tags?: { - edges?: Array<{ - node?: { - name?: string; - }; - }>; - }; - categories?: { - edges?: Array<{ - node?: { - name?: string; - slug?: string; - }; - }>; - }; - }; - }>; - pageInfo?: { - hasNextPage?: boolean; - endCursor?: string | null; - }; - }; -}; - -type PostNode = NonNullable< - NonNullable["edges"]>[number]["node"] ->; - -const STATIC_ROUTES: Array> = [ +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 }, @@ -126,387 +40,133 @@ const STATIC_ROUTES: Array> = [ { url: `${SITE_URL}/community/search`, changeFrequency: "weekly", priority: 0.6 }, ]; -// keep the latest successful sitemap in memory so request-time fallback is instant -// when the same runtime handles a later failure. -let lastSuccessfulSitemapXml: string | null = null; - -// hold the in-flight refresh promise so concurrent callers share one crawl instead -// of each starting an independent wordpress fetch sequence. -let refreshSitemapPromise: Promise | null = null; - -export type SitemapRefreshResult = { - entryCount: number; - generatedAt: string; - xml: string; -}; - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// retry statuses that are commonly temporary: -// - timeouts -// - rate limits -// - transient upstream/server failures -function isRetryableStatus(status: number) { - return [408, 429, 500, 502, 503, 504].includes(status); -} - -class RetryableFetchError extends Error { - constructor(message: string) { - super(message); - this.name = "RetryableFetchError"; - } -} - -function isRetryableRequestError(error: unknown) { - if (error instanceof RetryableFetchError) { - return true; - } - - if (error instanceof Error) { - if (error.name === "AbortError" || error.name === "TimeoutError") { - return true; - } - - if (error instanceof TypeError) { - return true; - } - } - - return false; -} - -async function fetchGraphQL(query: string, variables: Record = {}) { - // remember the last seen error so the final thrown error is meaningful. - let lastError: Error | null = null; - - for (let attempt = 1; attempt <= FETCH_RETRY_LIMIT; attempt += 1) { - try { - // send the graphql request to wordpress. - // - // the body contains: - // - query: the graphql query string - // - variables: query variables such as the pagination cursor - // - // the abort signal enforces a hard timeout for the request. - const response = await fetch(WP_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ query, variables }), - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), - }); - - if (!response.ok) { - if (isRetryableStatus(response.status) && attempt < FETCH_RETRY_LIMIT) { - throw new RetryableFetchError( - `WordPress GraphQL request failed with retryable status: ${response.status} ${response.statusText}` - ); - } - - // for non-retryable failures, or when retries are exhausted, fail immediately. - throw new Error(`WordPress GraphQL request failed: ${response.status} ${response.statusText}`); - } - - // parse the graphql response body after http success. - const json = (await response.json()) as GraphQLResponse; - - if (json.errors?.length) { - // graphql can return application-level errors even when the http response is 200. - const message = json.errors.map((error) => error.message).filter(Boolean).join(", "); - const graphqlErrorMessage = message || "WordPress GraphQL returned errors"; - - // treat graphql-level errors as retryable while attempts remain because - // wpgraphql can return transient errors (e.g. during plugin reload or db lock) - // with a 200 http response. failing immediately on first occurrence would miss - // these cases that the retry loop is specifically designed to handle. - if (attempt < FETCH_RETRY_LIMIT) { - throw new RetryableFetchError(graphqlErrorMessage); - } - throw new Error(graphqlErrorMessage); - } - - if (!json.data) { - // a graphql response without data is not useful for sitemap generation. - throw new Error("WordPress GraphQL returned no data"); - } - - // success path: return the typed graphql data. - return json.data; - } catch (error) { - // normalize unknown thrown values into an Error instance. - lastError = error instanceof Error ? error : new Error(String(error)); - - if (attempt < FETCH_RETRY_LIMIT && isRetryableRequestError(error)) { - // retry only transient failures (network/timeout/retryable upstream statuses). - await sleep(FETCH_RETRY_DELAY_MS * attempt); - continue; - } - - // fail fast for non-retryable errors and once retry attempts are exhausted. - throw lastError; - } - } - - throw lastError || new Error("WordPress GraphQL request failed"); -} +// 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; -function mapCategoriesToRoutes(categories?: PostNode["categories"]) { - // use a set so one post cannot produce duplicate routes even if wordpress returns - // the same category in multiple forms. +// 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(); - // map wordpress category data to real frontend route namespaces. - // - // we support matching by either slug or name because wordpress content can be - // inconsistent across environments or editorial changes. - if (slug === "technology" || name === "technology") { - routes.add("technology"); - } - - if (slug === "community" || name === "community") { - routes.add("community"); - } + if (slug === "technology" || name === "technology") routes.add("technology"); + if (slug === "community" || name === "community") routes.add("community"); } return Array.from(routes); } -async function fetchAllPosts() { - // collect only the posts that are eligible for sitemap inclusion. +// 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[] = []; - // graphql cursor pagination state. - let hasNextPage = true; - let after: string | null = null; - - while (hasNextPage) { - // fetch one page at a time from wordpress. - // - // query design: - // - first: 50 keeps payload size reasonable - // - after: cursor for the next page - // - orderby modified desc: newer posts are returned first - // - // fields requested: - // - slug: needed to build the final frontend url - // - modified: used to build sitemap lastmod - // - ppmaAuthorName: used to derive author archive urls - // - tags: used to derive tag archive urls from included posts - // - categories: used to map a wordpress post to technology/community routes - // - pageInfo: needed to continue pagination until every post is processed - const data = await fetchGraphQL( - ` - query SitemapPosts($after: String) { - posts( - first: ${POSTS_PAGE_SIZE} - after: $after - where: { - orderby: { field: MODIFIED, order: DESC } - } - ) { - edges { - node { - slug - modified - ppmaAuthorName - tags { - edges { - node { - name - } - } - } - categories { - edges { - node { - name - slug - } - } - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - `, - { after } - ); - - const edges = data.posts?.edges || []; - - for (const edge of edges) { - const node = edge?.node; - if (!node?.slug) { - // skip malformed wordpress records that do not have a usable slug. - continue; - } - - // decide whether this wordpress post belongs to a supported sitemap route. - // if the categories do not map to technology/community, the post is excluded. - const routes = mapCategoriesToRoutes(node.categories); - if (!routes.length) { - continue; - } - - // store the minimum data needed for later sitemap entry generation. - // - // note that we keep author and tag data here instead of running separate - // wordpress queries, because we only want author/tag pages backed by the - // exact set of posts that passed our inclusion rules. - posts.push({ - slug: node.slug, - modified: node.modified, - authorName: node.ppmaAuthorName, - routes, - tags: - node.tags?.edges - ?.map((tagEdge) => tagEdge?.node?.name?.trim()) - .filter((tagName): tagName is string => Boolean(tagName)) || [], - }); - } + 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)) || [], + }); + } - hasNextPage = Boolean(data.posts?.pageInfo?.hasNextPage); - const nextCursor = data.posts?.pageInfo?.endCursor || null; + return posts; +} - // guard against a malformed wordpress response that claims more pages exist but - // returns no cursor to advance to — without this, after stays null and the loop - // re-fetches the first page indefinitely until the function is killed. - if (hasNextPage && !nextCursor) { - throw new Error( - "WordPress pagination returned hasNextPage: true with no endCursor — cannot safely continue" - ); - } +// 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"); + } - after = nextCursor; + const technologyCount = posts.filter((p) => p.routes.includes("technology")).length; + const communityCount = posts.filter((p) => p.routes.includes("community")).length; - if (hasNextPage) { - // insert a small delay before fetching the next page to reduce pressure on wpgraphql. - await sleep(PAGE_SETTLE_DELAY_MS); - } + 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)` + ); } - - // return the full eligible post set after every page has been processed. - return posts; } function toIsoDate(value?: string) { - if (!value) { - return undefined; - } - - // convert the wordpress date string into a normalized iso string. - // why this exists: - // - wordpress may return a parseable date string format - // - the sitemap should emit a consistent machine-readable timestamp - // - invalid dates should quietly disappear instead of producing bad xml + 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; - } - + if (!isoDate) return false; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); - return new Date(isoDate) >= cutoff; } -function escapeXml(value: string) { - // escape xml-sensitive characters so urls and timestamps cannot break the xml document. - return value - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(//g, ">"); -} - -function dedupeEntries(entries: SitemapEntry[]) { - // keep only one entry per url in case multiple generation paths produce the same final url. - return Array.from(new Map(entries.map((entry) => [entry.url, entry])).values()); -} - -function getLatestModified(posts: SitemapPost[]) { - // find the newest modified timestamp across all included posts. - // this value is later used for high-level listing pages like /blog, /technology, - // and /community so those pages appear updated when the newest underlying post changes. +// 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; - } - + if (!current) return latest; + if (!latest || current > latest) return current; return latest; }, undefined); } -function buildPostEntries(posts: SitemapPost[]) { - // convert each included post into one or more sitemap entries. - // a single wordpress post can produce multiple urls if it belongs to both - // technology and community. +// 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) => ({ - // build the absolute public url using the site base url and the route namespace. url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`, - - // use the wordpress modified time as the sitemap lastmod. lastModified: toIsoDate(post.modified), - changeFrequency: "weekly" as const, - - // give newer posts slightly higher priority to hint at freshness. + // posts modified in the last 30 days get higher priority priority: isRecent(post.modified) ? 0.8 : 0.5, })) ); } -function buildAuthorEntries(posts: SitemapPost[]) { - // map author slug -> newest related post modified time. +// 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) { - // skip posts with no usable author name. - continue; - } + if (!authorName) continue; - // normalize the display name into the frontend author slug format. const authorSlug = sanitizeAuthorSlug(authorName); - if (!authorSlug) { - continue; - } + if (!authorSlug) continue; const currentModified = toIsoDate(post.modified); const existingModified = authorMap.get(authorSlug); - // keep the latest related post modification time so the author page lastmod - // reflects the freshest content shown on that author page. if (!existingModified || (currentModified && currentModified > existingModified)) { authorMap.set(authorSlug, currentModified); } } - // convert the author map into final sitemap entries. return Array.from(authorMap.entries()).map(([authorSlug, lastModified]) => ({ url: `${SITE_URL}/authors/${authorSlug}`, lastModified, @@ -515,8 +175,9 @@ function buildAuthorEntries(posts: SitemapPost[]) { })); } -function buildTagEntries(posts: SitemapPost[]) { - // map tag name -> newest related post modified time. +// 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) { @@ -524,26 +185,18 @@ function buildTagEntries(posts: SitemapPost[]) { for (const tagName of post.tags) { const normalizedTag = tagName.trim(); - if (!normalizedTag) { - // ignore empty or whitespace-only tag names from wordpress. - continue; - } + if (!normalizedTag) continue; const existingModified = tagMap.get(normalizedTag); - // keep the latest related post modification time so the tag page lastmod - // tracks the freshest post shown on that tag listing page. if (!existingModified || (postModified && postModified > existingModified)) { tagMap.set(normalizedTag, postModified); } } } - // convert the tag map into final sitemap entries. return Array.from(tagMap.entries()).flatMap(([tagName, lastModified]) => { const tagSlug = sanitizeStringForURL(tagName); - if (!tagSlug) { - return []; - } + if (!tagSlug) return []; return [{ url: `${SITE_URL}/tag/${tagSlug}`, lastModified, @@ -553,181 +206,49 @@ function buildTagEntries(posts: SitemapPost[]) { }); } -// minimum number of posts required per category before the sitemap is considered -// trustworthy enough to publish. this prevents a severely degraded wordpress -// response (e.g. first page only, partial crawl) from replacing a good snapshot. -const MIN_POSTS_PER_CATEGORY = 5; - -function assertFullSitemap(posts: SitemapPost[]) { - // enforce the "no partial publication" rule. - // if wordpress returns an obviously incomplete crawl, fail the refresh so the - // app can fall back to the last successful snapshot instead of publishing bad data. - if (!posts.length) { - throw new Error("Sitemap generation returned zero posts"); - } - - const technologyCount = posts.filter((post) => post.routes.includes("technology")).length; - const communityCount = posts.filter((post) => post.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)` - ); - } +// 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()); } -export async function getSitemapEntries() { - // crawl all eligible wordpress posts first. - const posts = await fetchAllPosts(); - - // do not continue unless the crawl looks complete enough to trust. - assertFullSitemap(posts); - - // use the newest included post update time as the lastmod for static listing pages. - const latestPostModified = getLatestModified(posts) || new Date().toISOString(); - const staticEntries = STATIC_ROUTES.map((entry) => ({ - ...entry, - lastModified: latestPostModified, - })); - - // combine every sitemap entry type and dedupe by final url. - return dedupeEntries([ - ...staticEntries, - ...buildPostEntries(posts), - ...buildAuthorEntries(posts), - ...buildTagEntries(posts), - ]); -} - -export function serializeSitemap(entries: SitemapEntry[]) { - // turn the structured entry objects into final sitemap xml. - - // this stays manual because: - // - pages router does not use app router metadata routes - // - we want full control over xml shape - // - we explicitly do not want xml comments in the output +// 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)}` : "", + typeof entry.priority === "number" + ? `${entry.priority.toFixed(1)}` + : "", ].filter(Boolean); return `${parts.join("")}`; }) .join(""); - return `` + - `${body}`; -} - -export function getStaticFallbackXml() { - // generate a minimal sitemap from the hardcoded static routes only. - // this requires no wordpress data and is used as a last-resort response - // when all three generation tiers fail (fresh, in-memory, and /tmp snapshot). - // it ensures crawlers always receive valid xml at /sitemap.xml rather than - // an html error page from next.js. - const now = new Date().toISOString(); - return serializeSitemap( - STATIC_ROUTES.map((route) => ({ ...route, lastModified: now })) - ); -} - -async function readPersistedSitemapSnapshot() { - try { - const xml = await fs.readFile(SITEMAP_SNAPSHOT_PATH, "utf8"); - // only trust the fallback file if it still looks like a sitemap document. - return isValidSitemapXml(xml) ? xml : null; - } catch { - // treat missing or unreadable snapshot files as "no fallback available". - return null; - } -} - -function isValidSitemapXml(xml: string) { - // perform a lightweight sanity check before serving a persisted snapshot. - // this is not full xml parsing; it only prevents obviously broken files from being used. - const normalized = xml.trim(); - return ( - normalized.startsWith(``) && - normalized.includes(``) && - normalized.endsWith(``) + `` + + `${body}` ); } -async function persistSitemapSnapshot(xml: string) { - try { - // write the latest good xml to the runtime-local snapshot path. - await fs.writeFile(SITEMAP_SNAPSHOT_PATH, xml, "utf8"); - } catch (error) { - // snapshot persistence is helpful but not critical enough to fail the whole refresh. - console.error( - `Failed to persist sitemap snapshot at ${SITEMAP_SNAPSHOT_PATH}. Check that the runtime allows writes to the temp directory and that sufficient permissions and storage are available:`, - error - ); - } -} - -export async function refreshSitemapSnapshot(): Promise { - if (!refreshSitemapPromise) { - refreshSitemapPromise = (async () => { - // run the full refresh pipeline: - // 1. fetch wordpress content - // 2. validate completeness - // 3. build entries - // 4. serialize xml - // 5. save success as the new fallback snapshot - const entries = await getSitemapEntries(); - const xml = serializeSitemap(entries); - - lastSuccessfulSitemapXml = xml; - await persistSitemapSnapshot(xml); - - return { - entryCount: entries.length, - generatedAt: new Date().toISOString(), - xml, - }; - })(); - - // once the refresh settles, clear the shared promise so future callers can - // start a new refresh instead of reusing an old completed one. - refreshSitemapPromise.finally(() => { - refreshSitemapPromise = null; - }); - } - - // if a refresh is already running, every caller waits on the same promise here. - return refreshSitemapPromise; +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(//g, ">"); } -export async function generateSitemapXml() { - try { - // first try to generate a fresh full sitemap from wordpress. - const result = await refreshSitemapSnapshot(); - return result.xml; - } catch (error) { - console.error( - `Fresh sitemap generation failed; attempting the last successful snapshot. Next steps: verify WORDPRESS_API_URL or NEXT_PUBLIC_WORDPRESS_API_URL is configured correctly and reachable at ${WP_API_URL}, then retry the sitemap refresh once WordPress GraphQL connectivity is restored.`, - error, - ); - - // first fallback: use the latest successful sitemap held in memory. - if (lastSuccessfulSitemapXml) { - return lastSuccessfulSitemapXml; - } - - // second fallback: use the runtime-local snapshot file if it exists and looks valid. - const persistedSnapshot = await readPersistedSitemapSnapshot(); - if (persistedSnapshot) { - lastSuccessfulSitemapXml = persistedSnapshot; - return persistedSnapshot; - } - - // if no fallback exists yet, propagate the error to the caller. - throw error; - } +// 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/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index 643685da..bf0b192e 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -3,133 +3,60 @@ import { isSearchConsoleSubmissionConfigured, submitSitemapToSearchConsole, } from "../../../lib/google-search-console"; -import { refreshSitemapSnapshot } from "../../../lib/sitemap"; -export const config = { - maxDuration: 300, -}; +// 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 }; -const GOOGLE_SUBMISSION_HELP = - "Verify GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY, " + - "GOOGLE_SEARCH_CONSOLE_SITE_URL, and Search Console property access for the service account."; - -export default async function refreshSitemap( - req: NextApiRequest, - res: NextApiResponse -) { - // read the bearer token supplied by vercel cron or a manual test caller. - const authHeader = req.headers.authorization; - - // this shared secret is how we decide whether the caller is allowed to trigger refreshes. +export default async function handler(req: NextApiRequest, res: NextApiResponse) { const expectedSecret = process.env.CRON_SECRET; - // fail with 500 if the deployment is missing CRON_SECRET entirely — this is a - // server misconfiguration, not a client auth problem, and should be diagnosed - // separately from a caller sending the wrong token. + // distinguish a deployment misconfiguration (500) from a wrong token (401). if (!expectedSecret) { console.error( - "CRON_SECRET is not configured. Next step: set CRON_SECRET in the Vercel environment variables, redeploy if required, then retry." + "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", + message: "Server misconfiguration — CRON_SECRET is not configured", }); } - // reject callers that do not provide the expected bearer token. - // - // note: vercel cron automatically includes an "Authorization: Bearer " - // header when CRON_SECRET is configured in the project settings. - if (authHeader !== `Bearer ${expectedSecret}`) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - }); + // 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" }); } - // limit the endpoint to get requests because vercel cron calls it with get and - // we do not need extra method surface area here. if (req.method !== "GET") { res.setHeader("Allow", "GET"); return res.status(405).json({ ok: false, message: "Method not allowed" }); } - try { - // step 1: generate a fresh sitemap snapshot from current wordpress data. - // if this fails, we return 500 and do not attempt google submission. - const result = await refreshSitemapSnapshot(); - - // this object reports whether the post-refresh google submission was attempted - // and whether it succeeded, but it does not control the success of the sitemap refresh itself. - let searchConsole: - | { - submitted: boolean; - submittedAt?: string; - sitemapUrl?: string; - siteUrl?: string; - skipped?: boolean; - message?: string; - } - | undefined; - - // step 2: only after the refresh succeeds, try to submit the sitemap url to - // google search console so google knows to fetch the updated sitemap again. - if (isSearchConsoleSubmissionConfigured()) { - try { - const submission = await submitSitemapToSearchConsole(); - searchConsole = { - submitted: true, - submittedAt: submission.submittedAt, - sitemapUrl: submission.sitemapUrl, - siteUrl: submission.siteUrl, - }; - } catch (error) { - console.error("Google Search Console sitemap submission failed:", error, GOOGLE_SUBMISSION_HELP); - - // do not fail the cron request here. - // the sitemap refresh already succeeded, and google submission should remain - // an optional follow-up step rather than a blocker. - searchConsole = { - submitted: false, - message: - error instanceof Error - ? `${error.message} ${GOOGLE_SUBMISSION_HELP}` - : `Google Search Console sitemap submission failed. ${GOOGLE_SUBMISSION_HELP}`, - }; - } - } else { - // skip the google step entirely if the required env vars are not configured. - searchConsole = { - submitted: false, - skipped: true, - message: "Google Search Console submission is not configured", - }; - } - + // skip silently if google search console env vars are not all configured. + if (!isSearchConsoleSubmissionConfigured()) { return res.status(200).json({ - // this means the sitemap refresh itself succeeded. ok: true, - - // how many final urls were generated into the refreshed sitemap. - entryCount: result.entryCount, - - // when the refreshed sitemap snapshot was generated. - generatedAt: result.generatedAt, - - // nested status for the optional google submission step. - searchConsole, + 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( - "Scheduled sitemap refresh failed. Next step: verify WordPress GraphQL reachability and WORDPRESS_API_URL, confirm CRON_SECRET is configured correctly, inspect preceding crawl logs, or rerun this endpoint manually:", + "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 ); - - // only core refresh failures should produce a 500 here. - // google submission failures are handled above and should not reach this block. return res.status(500).json({ ok: false, - message: "Sitemap refresh failed", + 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/sitemap.xml.ts b/pages/sitemap.xml.ts deleted file mode 100644 index 879d93bc..00000000 --- a/pages/sitemap.xml.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { GetServerSideProps } from "next"; -import { generateSitemapXml, getStaticFallbackXml } from "../lib/sitemap"; - -// give this route enough time to complete a fresh wordpress crawl before timing out. -// the cron handler has its own 300s budget; 60s is sufficient for user-facing requests -// because the retry budget is smaller and the typical crawl completes in under 20s. -export const config = { - maxDuration: 60, -}; - -function SitemapXml() { - // pages router still expects a component export even though we end the response manually. - return null; -} - -export const getServerSideProps: GetServerSideProps = async ({ res }) => { - let sitemap: string; - let statusCode = 200; - - try { - // generate a fresh sitemap xml if possible, or fall back to the latest successful snapshot. - sitemap = await generateSitemapXml(); - } catch (error) { - // all three generation tiers failed (fresh, in-memory snapshot, /tmp snapshot). - // this only happens on a cold start when wordpress is completely unreachable. - // serve the static-only fallback so crawlers always receive valid xml here - // instead of next.js's default html error page. - console.error( - "Sitemap generation failed with no snapshot available; serving static-routes-only fallback. " + - "Verify WordPress reachability and run the cron refresh endpoint once connectivity is restored.", - error, - ); - sitemap = getStaticFallbackXml(); - statusCode = 503; - } - - // tell clients and edge caches this response is xml, not html or json. - res.setHeader("Content-Type", "application/xml"); - - // only cache successful sitemaps at the edge for one day. - // do not cache degraded fallback responses so crawlers recover quickly once wordpress is back. - if (statusCode >= 500) { - res.setHeader("Cache-Control", "no-store"); - } else { - res.setHeader("Cache-Control", "public, max-age=0, s-maxage=86400, stale-while-revalidate=86400"); - } - - res.statusCode = statusCode; - - // write the raw xml directly into the response body. - res.write(sitemap); - - // finish the response manually because we are not rendering a normal page. - res.end(); - - return { - // next.js still expects a props object from getServerSideProps. - props: {}, - }; -}; - -export default SitemapXml; 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/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" + ] } From e2ef9170c808ad6dbae3da753c7e80c21d75e64f Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:06:13 +0530 Subject: [PATCH 27/28] feat: fix the 502 + fix strictnull checks Signed-off-by: amaan-bhati --- app/layout.tsx | 20 ++++-- app/not-found.tsx | 23 ++++++ app/sitemap.xml/route.ts | 146 +++++++++++++++++++++++++++++++-------- 3 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 app/not-found.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 1cbd70f0..afa67f16 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,17 @@ -// Required by Next.js App Router — every project with an app/ directory must have -// a root layout. All real layout (fonts, global CSS, scripts, structured data) -// lives in pages/_app.tsx and pages/_document.tsx, which continue to serve all -// existing pages/ routes unchanged. This layout only applies to routes inside app/. +// 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}; + 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 index 4963edb2..61871b73 100644 --- a/app/sitemap.xml/route.ts +++ b/app/sitemap.xml/route.ts @@ -1,4 +1,4 @@ -import { getAllPosts } from "../../lib/api"; +import https from "node:https"; import { adaptPostsForSitemap, assertFullSitemap, @@ -12,40 +12,129 @@ import { STATIC_ROUTES, } from "../../lib/sitemap"; -// ISR: Vercel caches this response in its persistent CDN edge cache for 1 hour. +// ISR: Vercel caches this response in its CDN edge cache for 1 hour. +// +// After first generation: every request 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 previous good version automatically. +// Cold-start / first request: mitigated by build-time pre-generation (see scripts/prewarm-sitemap.mjs) +// and post-deploy warming triggered by the Vercel deployment hook in GitHub Actions. +export const revalidate = 3600; -// What this means in practice: -// After the first generation, every request is served from CDN (<10ms, no Lambda invoked). -// After TTL expires: Vercel immediately serves the stale cached version to the -// requesting client, then regenerates in the background. The client never waits. -// If WordPress is down during background regen: Vercel silently keeps serving the -// previous cached version. No fallback code needed, it is platform behaviour. -// The only case where no cached version exists is the very first request after deploy, -// or if generation has never succeeded (WordPress down on cold start). this can be ignored for a while but we can add -// a fix for this as well, we can explore prewarming, while the build itself, if we add a cronjob +// --------------------------------------------------------------------------- +// fetchGraphQL — bypasses Next.js App Router's RSC fetch instrumentation. +// +// Problem: Next.js wraps global fetch() in RSC/Route Handler context with its +// own caching layer. This instrumented fetch sends headers that Cloudflare +// (in front of wp.keploy.io) interprets as invalid, returning 502 HTML instead +// of JSON — even though the same URL works fine from plain Node.js. +// +// Fix: use node:https directly. This is a raw TCP connection with no Next.js +// middleware in the path, identical to what curl or plain `node -e "fetch()"` sends. +// --------------------------------------------------------------------------- +function fetchGraphQL(query: string, variables: Record = {}): Promise { + const apiUrl = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL; + if (!apiUrl) throw new Error("WORDPRESS_API_URL is not configured"); -// without prewarming: User → triggers ISR → waits 13s ❌ -// prewarming: CI/CD → triggers ISR → warms cache ✅ -// User → hits CDN → instant response ✅ + const url = new URL(apiUrl); + const body = JSON.stringify({ query, variables }); -export const revalidate = 3600; + 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 (e) { + reject(new Error(`WordPress returned non-JSON (status ${res.statusCode}): ${data.slice(0, 120)}`)); + } + }); + } + ); + req.on("error", reject); + req.write(body); + req.end(); + }); +} + +// Paginates through all posts using cursor-based pagination. +// Identical query to lib/api.ts getAllPosts() but using fetchGraphQL instead of fetch(). +async function getAllPostsForSitemap() { + let allEdges: any[] = []; + let hasNextPage = true; + let endCursor: string | null = null; + + while (hasNextPage) { + const data = await fetchGraphQL( + ` + query AllPosts($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 }; +} export async function GET(): Promise { try { - // reuse the existing getAllPosts() paginator from lib/api.ts. - // as of the pagination fix, this fetches ALL posts (not just the first 50). - const allPostsResult = await getAllPosts(); - - // convert getAllPosts() return shape into SitemapPost[] for the entry builders. + const allPostsResult = await getAllPostsForSitemap(); const posts = adaptPostsForSitemap(allPostsResult); - // reject partial wordpress responses before they replace a good cached version. - // throws if fewer than 5 posts per category, ISR will not cache a thrown error, + // 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. + // Static routes get lastmod = newest post modification time. const latestModified = getLatestModified(posts) ?? new Date().toISOString(); const staticEntries = STATIC_ROUTES.map((r) => ({ ...r, @@ -65,9 +154,8 @@ export async function GET(): Promise { status: 200, headers: { "Content-Type": "application/xml", - // s-maxage instructs Vercel's CDN to cache for 1h (matches revalidate above). - // stale-while-revalidate lets the CDN serve stale while regenerating in background. - // max-age=0 ensures browsers always revalidate with the CDN rather than caching locally. + // 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", }, }); @@ -79,9 +167,7 @@ export async function GET(): Promise { ); // ISR does NOT cache non-2xx responses. - // Vercel will keep serving the previous good cached version on the CDN. - // no-store prevents any downstream proxy from caching this degraded response, - // so crawlers will retry on the next request once WordPress is back. + // no-store prevents any downstream proxy from caching this degraded response. return new Response(getStaticFallbackXml(), { status: 503, headers: { From 689aa7990de401fb0d580aadc9335736a2553b80 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:13:28 +0530 Subject: [PATCH 28/28] feat: create api server ts, server inly using node:https Signed-off-by: amaan-bhati --- app/sitemap.xml/route.ts | 122 ++++---------------------------------- lib/api-server.ts | 125 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 111 deletions(-) create mode 100644 lib/api-server.ts diff --git a/app/sitemap.xml/route.ts b/app/sitemap.xml/route.ts index 61871b73..fe129c88 100644 --- a/app/sitemap.xml/route.ts +++ b/app/sitemap.xml/route.ts @@ -1,4 +1,4 @@ -import https from "node:https"; +import { getAllPostsForSitemap } from "../../lib/api-server"; import { adaptPostsForSitemap, assertFullSitemap, @@ -14,119 +14,17 @@ import { // ISR: Vercel caches this response in its CDN edge cache for 1 hour. // -// After first generation: every request served from CDN (<10ms, no Lambda invoked). +// 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 previous good version automatically. -// Cold-start / first request: mitigated by build-time pre-generation (see scripts/prewarm-sitemap.mjs) -// and post-deploy warming triggered by the Vercel deployment hook in GitHub Actions. +// 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; -// --------------------------------------------------------------------------- -// fetchGraphQL — bypasses Next.js App Router's RSC fetch instrumentation. -// -// Problem: Next.js wraps global fetch() in RSC/Route Handler context with its -// own caching layer. This instrumented fetch sends headers that Cloudflare -// (in front of wp.keploy.io) interprets as invalid, returning 502 HTML instead -// of JSON — even though the same URL works fine from plain Node.js. -// -// Fix: use node:https directly. This is a raw TCP connection with no Next.js -// middleware in the path, identical to what curl or plain `node -e "fetch()"` sends. -// --------------------------------------------------------------------------- -function fetchGraphQL(query: string, variables: Record = {}): Promise { - const apiUrl = process.env.WORDPRESS_API_URL || process.env.NEXT_PUBLIC_WORDPRESS_API_URL; - if (!apiUrl) throw new Error("WORDPRESS_API_URL is not configured"); - - 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 (e) { - reject(new Error(`WordPress returned non-JSON (status ${res.statusCode}): ${data.slice(0, 120)}`)); - } - }); - } - ); - req.on("error", reject); - req.write(body); - req.end(); - }); -} - -// Paginates through all posts using cursor-based pagination. -// Identical query to lib/api.ts getAllPosts() but using fetchGraphQL instead of fetch(). -async function getAllPostsForSitemap() { - let allEdges: any[] = []; - let hasNextPage = true; - let endCursor: string | null = null; - - while (hasNextPage) { - const data = await fetchGraphQL( - ` - query AllPosts($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 }; -} - 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); @@ -134,7 +32,8 @@ export async function GET(): Promise { // so Vercel keeps serving the previous good cached version automatically. assertFullSitemap(posts); - // Static routes get lastmod = newest post modification time. + // 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, @@ -167,7 +66,8 @@ export async function GET(): Promise { ); // ISR does NOT cache non-2xx responses. - // no-store prevents any downstream proxy from caching this degraded response. + // 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: { 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 }; +}