From ad61c0a1477a3d93d87f5250442da825a0b1a44e Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 17:13:58 +0530 Subject: [PATCH 01/29] 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 | 254 ++++++++++++++++++++++++++++++ pages/api/cron/refresh-sitemap.ts | 62 ++++++++ pages/authors/[slug].tsx | 2 +- pages/community/[slug].tsx | 4 +- pages/technology/[slug].tsx | 4 +- tsconfig.json | 29 +++- 14 files changed, 490 insertions(+), 34 deletions(-) create mode 100644 app/layout.tsx create mode 100644 app/sitemap.xml/route.ts create mode 100644 lib/sitemap.ts create mode 100644 pages/api/cron/refresh-sitemap.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 new file mode 100644 index 00000000..b8446729 --- /dev/null +++ b/lib/sitemap.ts @@ -0,0 +1,254 @@ +import { SITE_URL } from "./structured-data"; +import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug"; +import { sanitizeStringForURL } from "../utils/sanitizeStringForUrl"; + +type SitemapChangeFrequency = "daily" | "weekly" | "monthly"; +type CategoryRoute = "technology" | "community"; + +export type SitemapEntry = { + // final absolute url that will appear inside . + url: string; + + // final normalized timestamp that will appear inside when available. + lastModified?: string; + + // optional sitemap hint for crawlers. + changeFrequency?: SitemapChangeFrequency; + + // optional sitemap hint for relative importance. + priority?: number; +}; + +// internal shape used by all entry builders. +// populated via adaptPostsForSitemap() from the getAllPosts() return value. +type SitemapPost = { + slug: string; + modified?: string; + authorName?: string; + tags: string[]; + routes: CategoryRoute[]; +}; + +export const STATIC_ROUTES: Array> = [ + // top-level listing and navigation pages that should always be in the sitemap. + { url: SITE_URL, changeFrequency: "daily", priority: 1.0 }, + { url: `${SITE_URL}/technology`, changeFrequency: "daily", priority: 0.9 }, + { url: `${SITE_URL}/community`, changeFrequency: "daily", priority: 0.9 }, + { url: `${SITE_URL}/authors`, changeFrequency: "weekly", priority: 0.8 }, + { url: `${SITE_URL}/tag`, changeFrequency: "weekly", priority: 0.8 }, + { url: `${SITE_URL}/search`, changeFrequency: "weekly", priority: 0.6 }, + { url: `${SITE_URL}/community/search`, changeFrequency: "weekly", priority: 0.6 }, +]; + +// minimum posts required per category before the sitemap is considered trustworthy. +// prevents a degraded partial wordpress response from replacing a good cached version. +const MIN_POSTS_PER_CATEGORY = 5; + +// maps wordpress category data to the two supported frontend route namespaces. +// matches by both slug and name (lowercased) to handle editorial inconsistencies. +function mapCategoriesToRoutes(categories?: { + edges?: Array<{ node?: { name?: string; slug?: string } }>; +}): CategoryRoute[] { + const routes = new Set(); + + for (const edge of categories?.edges || []) { + const slug = edge?.node?.slug?.trim()?.toLowerCase(); + const name = edge?.node?.name?.trim()?.toLowerCase(); + + if (slug === "technology" || name === "technology") routes.add("technology"); + if (slug === "community" || name === "community") routes.add("community"); + } + + return Array.from(routes); +} + +// converts the getAllPosts() return shape into SitemapPost[]. +// this is the only coupling point between lib/api.ts and lib/sitemap.ts. +// posts with no matching category route are excluded — same rule as before. +export function adaptPostsForSitemap(allPostsResult: { + edges: Array<{ node: any }>; +}): SitemapPost[] { + const posts: SitemapPost[] = []; + + for (const edge of allPostsResult?.edges || []) { + const node = edge?.node; + if (!node?.slug) continue; + + const routes = mapCategoriesToRoutes(node.categories); + if (!routes.length) continue; + + posts.push({ + slug: node.slug, + modified: node.modified, + authorName: node.ppmaAuthorName, + routes, + tags: + node.tags?.edges + ?.map((e: any) => e?.node?.name?.trim()) + .filter((n: unknown): n is string => Boolean(n)) || [], + }); + } + + return posts; +} + +// throws if the crawl looks incomplete, preventing partial data from being cached. +export function assertFullSitemap(posts: SitemapPost[]) { + if (!posts.length) { + throw new Error("Sitemap generation returned zero posts"); + } + + const technologyCount = posts.filter((p) => p.routes.includes("technology")).length; + const communityCount = posts.filter((p) => p.routes.includes("community")).length; + + if (technologyCount < MIN_POSTS_PER_CATEGORY || communityCount < MIN_POSTS_PER_CATEGORY) { + throw new Error( + `Sitemap generation incomplete: technology=${technologyCount}, community=${communityCount} (minimum ${MIN_POSTS_PER_CATEGORY} required per category)` + ); + } +} + +function toIsoDate(value?: string) { + if (!value) return undefined; + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString(); +} + +function isRecent(dateValue?: string, days = 30) { + const isoDate = toIsoDate(dateValue); + if (!isoDate) return false; + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - days); + return new Date(isoDate) >= cutoff; +} + +// finds the newest modified timestamp across all included posts. +// used to set lastmod on static listing pages so they reflect freshest content. +export function getLatestModified(posts: SitemapPost[]) { + return posts.reduce((latest, post) => { + const current = toIsoDate(post.modified); + if (!current) return latest; + if (!latest || current > latest) return current; + return latest; + }, undefined); +} + +// one entry per post per matching route. +// a post in both technology and community produces two urls. +export function buildPostEntries(posts: SitemapPost[]): SitemapEntry[] { + return posts.flatMap((post) => + post.routes.map((route) => ({ + url: `${SITE_URL}/${route}/${encodeURIComponent(post.slug)}`, + lastModified: toIsoDate(post.modified), + changeFrequency: "weekly" as const, + // posts modified in the last 30 days get higher priority + priority: isRecent(post.modified) ? 0.8 : 0.5, + })) + ); +} + +// one entry per unique author derived from included posts. +// lastmod is the newest modification time of any post by that author. +export function buildAuthorEntries(posts: SitemapPost[]): SitemapEntry[] { + const authorMap = new Map(); + + for (const post of posts) { + const authorName = post.authorName?.trim(); + if (!authorName) continue; + + const authorSlug = sanitizeAuthorSlug(authorName); + if (!authorSlug) continue; + + const currentModified = toIsoDate(post.modified); + const existingModified = authorMap.get(authorSlug); + + if (!existingModified || (currentModified && currentModified > existingModified)) { + authorMap.set(authorSlug, currentModified); + } + } + + return Array.from(authorMap.entries()).map(([authorSlug, lastModified]) => ({ + url: `${SITE_URL}/authors/${authorSlug}`, + lastModified, + changeFrequency: "weekly" as const, + priority: 0.7, + })); +} + +// one entry per unique tag derived from included posts. +// lastmod is the newest modification time of any post with that tag. +export function buildTagEntries(posts: SitemapPost[]): SitemapEntry[] { + const tagMap = new Map(); + + for (const post of posts) { + const postModified = toIsoDate(post.modified); + + for (const tagName of post.tags) { + const normalizedTag = tagName.trim(); + if (!normalizedTag) continue; + + const existingModified = tagMap.get(normalizedTag); + if (!existingModified || (postModified && postModified > existingModified)) { + tagMap.set(normalizedTag, postModified); + } + } + } + + return Array.from(tagMap.entries()).flatMap(([tagName, lastModified]) => { + const tagSlug = sanitizeStringForURL(tagName); + if (!tagSlug) return []; + return [{ + url: `${SITE_URL}/tag/${tagSlug}`, + lastModified, + changeFrequency: "weekly" as const, + priority: 0.7, + }]; + }); +} + +// keeps one entry per url — last writer wins if two paths produce the same url. +export function dedupeEntries(entries: SitemapEntry[]): SitemapEntry[] { + return Array.from(new Map(entries.map((e) => [e.url, e])).values()); +} + +// serializes sitemap entries to xml manually — no external library. +// all values are escaped. priority is formatted to one decimal place. +export function serializeSitemap(entries: SitemapEntry[]): string { + const body = entries + .map((entry) => { + const parts = [ + `${escapeXml(entry.url)}`, + entry.lastModified ? `${escapeXml(entry.lastModified)}` : "", + entry.changeFrequency ? `${entry.changeFrequency}` : "", + typeof entry.priority === "number" + ? `${entry.priority.toFixed(1)}` + : "", + ].filter(Boolean); + + return `${parts.join("")}`; + }) + .join(""); + + return ( + `` + + `${body}` + ); +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(//g, ">"); +} + +// last-resort xml served when generation fails with no cached version available. +// contains only the 7 hardcoded static routes — no wordpress data required. +export function getStaticFallbackXml(): string { + const now = new Date().toISOString(); + return serializeSitemap( + STATIC_ROUTES.map((route) => ({ ...route, lastModified: now })) + ); +} diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts new file mode 100644 index 00000000..bf0b192e --- /dev/null +++ b/pages/api/cron/refresh-sitemap.ts @@ -0,0 +1,62 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { + isSearchConsoleSubmissionConfigured, + submitSitemapToSearchConsole, +} from "../../../lib/google-search-console"; + +// GSC submission is fast — no WordPress crawl happens here anymore. +// Sitemap generation is handled by ISR in app/sitemap.xml/route.ts. +export const config = { maxDuration: 30 }; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const expectedSecret = process.env.CRON_SECRET; + + // distinguish a deployment misconfiguration (500) from a wrong token (401). + if (!expectedSecret) { + console.error( + "CRON_SECRET is not configured. Set it in Vercel environment variables and redeploy." + ); + return res.status(500).json({ + ok: false, + message: "Server misconfiguration — CRON_SECRET is not configured", + }); + } + + // auth is checked before method to avoid leaking valid HTTP methods to + // unauthenticated callers. vercel cron automatically injects this header. + if (req.headers.authorization !== `Bearer ${expectedSecret}`) { + return res.status(401).json({ ok: false, message: "Unauthorized" }); + } + + if (req.method !== "GET") { + res.setHeader("Allow", "GET"); + return res.status(405).json({ ok: false, message: "Method not allowed" }); + } + + // skip silently if google search console env vars are not all configured. + if (!isSearchConsoleSubmissionConfigured()) { + return res.status(200).json({ + ok: true, + message: "Google Search Console submission is not configured — skipped", + }); + } + + try { + // notify google that the sitemap has been updated so it re-crawls it. + // the sitemap itself is generated and cached by ISR — no crawl needed here. + const result = await submitSitemapToSearchConsole(); + return res.status(200).json({ ok: true, ...result }); + } catch (error) { + console.error( + "Google Search Console sitemap submission failed. " + + "Verify GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY, " + + "GOOGLE_SEARCH_CONSOLE_SITE_URL, and Search Console property access for the service account.", + error + ); + return res.status(500).json({ + ok: false, + message: + error instanceof Error ? error.message : "Google Search Console submission failed", + }); + } +} diff --git a/pages/authors/[slug].tsx b/pages/authors/[slug].tsx index 67609d52..8f39a232 100644 --- a/pages/authors/[slug].tsx +++ b/pages/authors/[slug].tsx @@ -111,7 +111,7 @@ export const getStaticProps: GetStaticProps = async ({ candidateAuthorNames.add(slugWords[0]); } - let filteredPosts = []; + let filteredPosts: any[] = []; for (const candidate of Array.from(candidateAuthorNames)) { if (!candidate) continue; diff --git a/pages/community/[slug].tsx b/pages/community/[slug].tsx index c83d6534..e139d8f9 100644 --- a/pages/community/[slug].tsx +++ b/pages/community/[slug].tsx @@ -85,7 +85,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) { }, ]; - const postBodyRef = useRef(); + const postBodyRef = useRef(null); const readProgress = useSpringValue(0); useScroll({ onChange(v) { @@ -148,7 +148,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) { const safeDescription = getSafeDescription(router.isFallback, post?.seo?.metaDesc, safeTitle); const postUrl = post?.slug ? `${SITE_URL}/community/${post.slug}` : `${SITE_URL}/community`; - const structuredData = []; + const structuredData: any[] = []; if (post?.slug) { structuredData.push( getBreadcrumbListSchema([ diff --git a/pages/technology/[slug].tsx b/pages/technology/[slug].tsx index 44413bb4..ffd20a69 100644 --- a/pages/technology/[slug].tsx +++ b/pages/technology/[slug].tsx @@ -81,7 +81,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) { description: reviewAuthorDescription || "A Reviewer for keploy's blog", }, ]; - const postBodyRef = useRef(); + const postBodyRef = useRef(null); const readProgress = useSpringValue(0); useScroll({ onChange(v) { @@ -138,7 +138,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) { const safeDescription = getSafeDescription(router.isFallback, post?.seo?.metaDesc, safeTitle); const postUrl = post?.slug ? `${SITE_URL}/technology/${post.slug}` : `${SITE_URL}/technology`; - const structuredData = []; + const structuredData: any[] = []; if (post?.slug) { structuredData.push( getBreadcrumbListSchema([ diff --git a/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 d055f067d797aa0cc62d9b382578efbf3ee73223 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:06:13 +0530 Subject: [PATCH 02/29] 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 600e65f7816b317fd34b9618ea3237ad3d16b62d Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:13:28 +0530 Subject: [PATCH 03/29] 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 }; +} From 2650275f1156e817b84874b10f86bd44bcf83c51 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:29:40 +0530 Subject: [PATCH 04/29] feat: add post-deploy ISR sitemap prewarming workflow Adds a GitHub Actions workflow triggered by a Vercel deploy hook that hits /blog/sitemap.xml immediately after deployment, warming the ISR cache so the first real user or crawler never hits a cold Lambda. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/prewarm-sitemap.yml | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/prewarm-sitemap.yml diff --git a/.github/workflows/prewarm-sitemap.yml b/.github/workflows/prewarm-sitemap.yml new file mode 100644 index 00000000..a83e03af --- /dev/null +++ b/.github/workflows/prewarm-sitemap.yml @@ -0,0 +1,53 @@ +name: Prewarm Sitemap After Deploy + +# Triggered by Vercel via a repository_dispatch event. +# In Vercel: Settings → Git → Deploy Hooks → create a hook named "prewarm-sitemap" +# targeting this branch. Vercel calls it automatically after each successful deployment. +# +# Alternatively, trigger manually: Actions → Prewarm Sitemap After Deploy → Run workflow +on: + repository_dispatch: + types: [vercel-deploy-complete] + workflow_dispatch: + inputs: + environment: + description: "Target environment (production or preview)" + required: false + default: "production" + +jobs: + prewarm: + name: Warm ISR sitemap cache + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Wait for Vercel edge propagation + # Give Vercel 30s to propagate the new deployment to all edge nodes + # before we hit the sitemap endpoint. + run: sleep 30 + + - name: Warm sitemap (production) + # Hitting the sitemap URL causes Vercel to run the ISR route handler, + # generate the full sitemap from WordPress, and cache it on the CDN. + # Every subsequent request — including the first real user or crawler — + # is served from CDN (<10ms) with no cold-start latency. + run: | + echo "Warming https://keploy.io/blog/sitemap.xml ..." + HTTP_STATUS=$(curl -s -o /tmp/sitemap-response.xml \ + -w "%{http_code}" \ + --max-time 120 \ + "https://keploy.io/blog/sitemap.xml") + + echo "HTTP status: $HTTP_STATUS" + + if [ "$HTTP_STATUS" = "200" ]; then + URL_COUNT=$(grep -c "" /tmp/sitemap-response.xml || echo 0) + echo "Sitemap warmed successfully — $URL_COUNT URLs" + else + echo "Warning: sitemap returned HTTP $HTTP_STATUS (ISR fallback may be active)" + echo "WordPress may be temporarily unavailable. Vercel will serve" + echo "the previous cached version to crawlers. Will auto-recover on next ISR cycle." + # Do NOT fail the workflow — a 503 is expected if WordPress is briefly down. + # The ISR fallback is the safety net; this job is best-effort warming only. + fi From a37b8c2f320103965e5f1fddc0780923816c43cb Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:39:20 +0530 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20add=20missing=20files=20=E2=80=94?= =?UTF-8?q?=20GSC=20lib,=20vercel=20cron,=20CSP=20fixes,=20playwright=20en?= =?UTF-8?q?v?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/google-search-console.ts: OAuth2 JWT flow for GSC submission, required by pages/api/cron/refresh-sitemap.ts - scripts/submit-sitemap-to-search-console.mjs: local dev script to manually submit sitemap to GSC without deploying - vercel.json: add cron schedule (daily midnight), add missing redirect, fix CSP header source regex to exclude sitemap.xml and /api/ paths - next.config.js: exclude sitemap.xml from Next.js CSP headers to keep both layers consistent with vercel.json - playwright.config.ts: inject CRON_SECRET=test-secret so e2e cron tests can authenticate against the local dev server Co-Authored-By: Claude Sonnet 4.6 --- lib/google-search-console.ts | 144 +++++++++++++++++ next.config.js | 4 +- playwright.config.ts | 1 + scripts/submit-sitemap-to-search-console.mjs | 160 +++++++++++++++++++ vercel.json | 13 +- 5 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 lib/google-search-console.ts create mode 100644 scripts/submit-sitemap-to-search-console.mjs diff --git a/lib/google-search-console.ts b/lib/google-search-console.ts new file mode 100644 index 00000000..315555ac --- /dev/null +++ b/lib/google-search-console.ts @@ -0,0 +1,144 @@ +import crypto from "crypto"; +import { SITE_URL } from "./structured-data"; + +const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token"; +const GOOGLE_WEBMASTERS_SCOPE = "https://www.googleapis.com/auth/webmasters"; +const GOOGLE_SITEMAPS_SUBMIT_BASE_URL = "https://www.googleapis.com/webmasters/v3/sites"; +const GOOGLE_TOKEN_LIFETIME_SECONDS = 3600; +const GOOGLE_FETCH_TIMEOUT_MS = 25000; + +function getRequiredEnv(name: string) { + const value = process.env[name]?.trim(); + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + + return value; +} + +function base64UrlEncode(value: Buffer | string) { + return Buffer.from(value) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +} + +function getGooglePrivateKey() { + return getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY").replace(/\\n/g, "\n"); +} + +function createServiceAccountJwt() { + const clientEmail = getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL"); + const now = Math.floor(Date.now() / 1000); + + const header = { + alg: "RS256", + typ: "JWT", + }; + + const payload = { + iss: clientEmail, + scope: GOOGLE_WEBMASTERS_SCOPE, + aud: GOOGLE_OAUTH_TOKEN_URL, + exp: now + GOOGLE_TOKEN_LIFETIME_SECONDS, + iat: now, + }; + + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(payload)); + const unsignedToken = `${encodedHeader}.${encodedPayload}`; + + const signer = crypto.createSign("RSA-SHA256"); + signer.update(unsignedToken); + signer.end(); + + const signature = signer.sign(getGooglePrivateKey()); + + return `${unsignedToken}.${base64UrlEncode(signature)}`; +} + +async function fetchGoogleAccessToken() { + const assertion = createServiceAccountJwt(); + + const response = await fetch(GOOGLE_OAUTH_TOKEN_URL, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion, + }), + signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS), + }); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ""); + throw new Error( + `Google OAuth token request failed: ${response.status} ${response.statusText}${ + errorBody ? ` - ${errorBody}` : "" + }` + ); + } + + const json = (await response.json()) as { + access_token?: string; + }; + + if (!json.access_token) { + throw new Error("Google OAuth token response did not include an access token"); + } + + return json.access_token; +} + +function getSearchConsoleSiteUrl() { + return getRequiredEnv("GOOGLE_SEARCH_CONSOLE_SITE_URL"); +} + +function getSitemapUrl() { + return process.env.SITEMAP_PUBLIC_URL?.trim() || `${SITE_URL}/sitemap.xml`; +} + +export function isSearchConsoleSubmissionConfigured() { + return Boolean( + process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL?.trim() && + process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY?.trim() && + process.env.GOOGLE_SEARCH_CONSOLE_SITE_URL?.trim() + ); +} + +export async function submitSitemapToSearchConsole() { + const accessToken = await fetchGoogleAccessToken(); + const siteUrl = getSearchConsoleSiteUrl(); + const sitemapUrl = getSitemapUrl(); + + const response = await fetch( + `${GOOGLE_SITEMAPS_SUBMIT_BASE_URL}/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent( + sitemapUrl + )}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS), + } + ); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ""); + throw new Error( + `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${ + errorBody ? ` - ${errorBody}` : "" + }` + ); + } + + return { + siteUrl, + sitemapUrl, + submittedAt: new Date().toISOString(), + }; +} diff --git a/next.config.js b/next.config.js index 38bdc6b9..ccb795a8 100644 --- a/next.config.js +++ b/next.config.js @@ -53,7 +53,9 @@ module.exports = { async headers() { return [ { - source: '/(.*)', + // exclude sitemap.xml — xml documents do not use csp and vercel.json + // also excludes it, so keep both layers consistent. + source: '/((?!sitemap\\.xml$).*)', headers: [ { key: 'Content-Security-Policy', diff --git a/playwright.config.ts b/playwright.config.ts index 237f3b15..984b33fc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -116,6 +116,7 @@ export default defineConfig({ env: { WORDPRESS_API_URL: GRAPHQL_API_URL, NEXT_PUBLIC_WORDPRESS_API_URL: GRAPHQL_API_URL, + CRON_SECRET: 'test-secret', }, }, ], diff --git a/scripts/submit-sitemap-to-search-console.mjs b/scripts/submit-sitemap-to-search-console.mjs new file mode 100644 index 00000000..24bb2ca1 --- /dev/null +++ b/scripts/submit-sitemap-to-search-console.mjs @@ -0,0 +1,160 @@ +import crypto from "crypto"; +import dotenv from "dotenv"; + +// load .env.local explicitly so this standalone script behaves like the local app +// and can reuse the same credentials without extra shell setup. +dotenv.config({ path: ".env.local" }); + +const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token"; +const GOOGLE_WEBMASTERS_SCOPE = "https://www.googleapis.com/auth/webmasters"; +const GOOGLE_SITEMAPS_SUBMIT_BASE_URL = "https://www.googleapis.com/webmasters/v3/sites"; +const GOOGLE_TOKEN_LIFETIME_SECONDS = 3600; +const GOOGLE_FETCH_TIMEOUT_MS = 25000; +const DEFAULT_SITEMAP_URL = "https://keploy.io/blog/sitemap.xml"; + +function getRequiredEnv(name) { + // fail early if any required local env var is missing. + const value = process.env[name]?.trim(); + + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + + return value; +} + +function base64UrlEncode(value) { + // convert to jwt-safe base64url form. + return Buffer.from(value) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +} + +function getGooglePrivateKey() { + // restore newline characters so the pem key is valid for crypto signing. + return getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY").replace(/\\n/g, "\n"); +} + +function createServiceAccountJwt() { + const clientEmail = getRequiredEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL"); + const now = Math.floor(Date.now() / 1000); + + // construct the service-account jwt the same way the app code does so this + // script is a faithful local verification path. + const header = { + alg: "RS256", + typ: "JWT", + }; + + const payload = { + iss: clientEmail, + scope: GOOGLE_WEBMASTERS_SCOPE, + aud: GOOGLE_OAUTH_TOKEN_URL, + exp: now + GOOGLE_TOKEN_LIFETIME_SECONDS, + iat: now, + }; + + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(payload)); + const unsignedToken = `${encodedHeader}.${encodedPayload}`; + + const signer = crypto.createSign("RSA-SHA256"); + signer.update(unsignedToken); + signer.end(); + + const signature = signer.sign(getGooglePrivateKey()); + + return `${unsignedToken}.${base64UrlEncode(signature)}`; +} + +async function fetchGoogleAccessToken() { + // exchange the signed jwt for a short-lived oauth token. + const assertion = createServiceAccountJwt(); + + const response = await fetch(GOOGLE_OAUTH_TOKEN_URL, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion, + }), + signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS), + }); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ""); + throw new Error( + `Google OAuth token request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : "" + }` + ); + } + + const json = await response.json(); + + if (!json.access_token) { + throw new Error("Google OAuth token response did not include an access token"); + } + + return json.access_token; +} + +async function submitSitemapToSearchConsole() { + // get the token, the property id, and the sitemap url from local env. + const accessToken = await fetchGoogleAccessToken(); + const siteUrl = getRequiredEnv("GOOGLE_SEARCH_CONSOLE_SITE_URL"); + const sitemapUrl = process.env.SITEMAP_PUBLIC_URL?.trim() || DEFAULT_SITEMAP_URL; + + // submit the sitemap url directly to google search console so we can verify + // the credentials and property access without going through the cron route. + const response = await fetch( + `${GOOGLE_SITEMAPS_SUBMIT_BASE_URL}/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent( + sitemapUrl + )}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + signal: AbortSignal.timeout(GOOGLE_FETCH_TIMEOUT_MS), + } + ); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ""); + throw new Error( + `Google Search Console sitemap submission failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : "" + }` + ); + } + + return { + ok: true, + siteUrl, + sitemapUrl, + submittedAt: new Date().toISOString(), + }; +} + +try { + // print a compact success object so the local test result is easy to inspect. + const result = await submitSitemapToSearchConsole(); + console.log(JSON.stringify(result, null, 2)); +} catch (error) { + // print a compact structured failure object for quick debugging. + console.error( + JSON.stringify( + { + ok: false, + message: error instanceof Error ? error.message : String(error), + }, + null, + 2 + ) + ); + process.exit(1); +} + diff --git a/vercel.json b/vercel.json index ab5cb8be..359f525e 100644 --- a/vercel.json +++ b/vercel.json @@ -1,10 +1,21 @@ { + "crons": [ + { + "path": "/blog/api/cron/refresh-sitemap", + "schedule": "0 0 * * *" + } + ], "redirects": [ { "source": "/blog/community/everything-you-need-to-know-about-api-testing", "destination": "/blog/community/what-is-api-testing", "permanent": true }, + { + "source": "/blog/community/end-to-end-testing-and-why-do-you-need-it", + "destination": "/blog/community/end-to-end-testing-guide", + "permanent": true + }, { "source": "/blog/community/regression-testing-tools-rankings-2025", "destination": "/blog/community/regression-testing-tools", @@ -41,7 +52,7 @@ ] }, { - "source": "/blog/(.*)", + "source": "/blog/((?!(?:sitemap\\.xml$|api/|_next/static/)).*)", "headers": [ { "key": "Content-Security-Policy", From eef57e57e60168e94eae318e2dec810c39f17dd1 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:45:26 +0530 Subject: [PATCH 06/29] tests: add e2e tests for ISR sitemap and cron refresh endpoint Cover the full ISR sitemap flow: - Sitemap.spec.ts: status 200, Content-Type xml, correct s-maxage=3600 ISR cache headers, valid urlset structure, static routes presence, dynamic post count, lastmod dates, changefreq, CSP exclusion, and URL deduplication - RefreshSitemapCron.spec.ts: 401 without auth, 401 with wrong secret, 405 for non-GET (method not leaked to unauthenticated callers), 200 skipped response when GSC env vars are absent in test env, and no-store cache-control check Co-Authored-By: Claude Sonnet 4.6 --- tests/e2e/RefreshSitemapCron.spec.ts | 67 ++++++++++++++++++++++++++ tests/e2e/Sitemap.spec.ts | 72 ++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 tests/e2e/RefreshSitemapCron.spec.ts create mode 100644 tests/e2e/Sitemap.spec.ts diff --git a/tests/e2e/RefreshSitemapCron.spec.ts b/tests/e2e/RefreshSitemapCron.spec.ts new file mode 100644 index 00000000..6c4b4cae --- /dev/null +++ b/tests/e2e/RefreshSitemapCron.spec.ts @@ -0,0 +1,67 @@ +import { expect, test } from '@playwright/test'; + +// CRON_SECRET is injected as 'test-secret' via playwright.config.ts webServer env. +// These tests call the Pages API route directly using the Playwright request fixture. + +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 has wrong secret', 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 POST with valid Authorization', async ({ request, baseURL }) => { + // Auth is checked before method guard — valid token must be used to reach the method check + 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 for authorized GET when GSC is not configured', async ({ request, baseURL }) => { + // In the test environment Google Search Console env vars are not set, + // so the handler skips GSC submission and returns a "skipped" success. + 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); + // GSC not configured in test env — handler returns a message, not siteUrl/sitemapUrl + expect(typeof body.message).toBe('string'); + }); + + test('unauthenticated callers get 401, not 405 — method not leaked', async ({ request, baseURL }) => { + // Security: method guard must not run before auth check so that + // unauthenticated callers cannot probe which HTTP methods are valid. + const response = await request.post(`${baseURL}/api/cron/refresh-sitemap`); + expect(response.status()).toBe(401); + }); + + test('Cache-Control header prevents caching of cron response', async ({ request, baseURL }) => { + // The /api/ prefix routes get Cache-Control: no-store via vercel.json headers rule. + const response = await request.get(`${baseURL}/api/cron/refresh-sitemap`, { + headers: { Authorization: 'Bearer test-secret' }, + }); + // Vercel injects no-store for all /blog/api/* routes + const cc = response.headers()['cache-control'] ?? ''; + expect(cc).toContain('no-store'); + }); +}); diff --git a/tests/e2e/Sitemap.spec.ts b/tests/e2e/Sitemap.spec.ts new file mode 100644 index 00000000..5c0e0590 --- /dev/null +++ b/tests/e2e/Sitemap.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Sitemap ISR Route', () => { + test('/sitemap.xml returns 200 with correct Content-Type', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toContain('application/xml'); + }); + + test('/sitemap.xml has correct ISR Cache-Control headers', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const cc = response.headers()['cache-control'] ?? ''; + // ISR: CDN caches 1h, browsers always revalidate, stale-while-revalidate 1h + expect(cc).toContain('s-maxage=3600'); + expect(cc).toContain('max-age=0'); + expect(cc).toContain('stale-while-revalidate=3600'); + }); + + test('/sitemap.xml contains valid XML sitemap structure', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + expect(xml).toContain(''); + expect(xml).toContain(''); + expect(xml).toContain(''); + }); + + test('/sitemap.xml includes all static routes', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + expect(xml).toContain('https://keploy.io/blog'); + expect(xml).toContain('https://keploy.io/blog/technology'); + expect(xml).toContain('https://keploy.io/blog/community'); + }); + + test('/sitemap.xml contains dynamic post URLs from WordPress', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + // Dynamic posts are served from WordPress via ISR — should contain at least 1 post URL + const locMatches = xml.match(//g) ?? []; + expect(locMatches.length).toBeGreaterThan(5); + }); + + test('/sitemap.xml entries have dates', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + // Every block should include a + expect(xml).toContain(''); + // lastmod should be a valid ISO date fragment (YYYY-MM-DD) + expect(xml).toMatch(/\d{4}-\d{2}-\d{2}/); + }); + + test('/sitemap.xml entries have ', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + expect(xml).toContain(''); + }); + + test('sitemap.xml is not served with a Content-Security-Policy header', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + // CSP is intentionally excluded from sitemap (XML parsers do not understand it) + const csp = response.headers()['content-security-policy']; + expect(csp).toBeUndefined(); + }); + + test('/sitemap.xml URLs are deduplicated', async ({ request, baseURL }) => { + const response = await request.get(`${baseURL}/sitemap.xml`); + const xml = await response.text(); + const locs = [...xml.matchAll(/(.*?)<\/loc>/g)].map(m => m[1]); + const unique = new Set(locs); + expect(unique.size).toBe(locs.length); + }); +}); From 997fb878b29797c01b418650358327263c3cfd82 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 20:49:39 +0530 Subject: [PATCH 07/29] =?UTF-8?q?fix:=20remove=20public/sitemap.xml=20?= =?UTF-8?q?=E2=80=94=20conflicts=20with=20app/sitemap.xml/route.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js raises a build error when a static file in public/ and a route handler exist at the same path. The ISR route handler in app/sitemap.xml/route.ts supersedes this stale 2024 static file. Co-Authored-By: Claude Sonnet 4.6 --- public/sitemap.xml | 1571 -------------------------------------------- 1 file changed, 1571 deletions(-) delete mode 100644 public/sitemap.xml 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 From 7a9a9a38eba8ae651b0b5dd67f4ec42bb9a3d9e6 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 21:13:05 +0530 Subject: [PATCH 08/29] feat: fix and address copilot review#0 Signed-off-by: amaan-bhati --- .github/workflows/prewarm-sitemap.yml | 9 ++++----- lib/api-server.ts | 11 +++++++++-- lib/sitemap.ts | 6 +++++- package-lock.json | 7 +++++++ package.json | 1 + playwright.config.ts | 3 +++ tests/e2e/Sitemap.spec.ts | 2 +- 7 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prewarm-sitemap.yml b/.github/workflows/prewarm-sitemap.yml index a83e03af..5e616c24 100644 --- a/.github/workflows/prewarm-sitemap.yml +++ b/.github/workflows/prewarm-sitemap.yml @@ -9,11 +9,10 @@ on: repository_dispatch: types: [vercel-deploy-complete] workflow_dispatch: - inputs: - environment: - description: "Target environment (production or preview)" - required: false - default: "production" + +# This workflow always warms the production sitemap URL. +# It is not intended for preview deployments — preview URLs are ephemeral +# and do not benefit from CDN warming. jobs: prewarm: diff --git a/lib/api-server.ts b/lib/api-server.ts index b13c1a52..1f731616 100644 --- a/lib/api-server.ts +++ b/lib/api-server.ts @@ -13,6 +13,8 @@ * does not exist in the browser. All client-side API calls go through * lib/api.ts which uses the standard fetch() API. */ +import "server-only"; +import http from "node:http"; import https from "node:https"; function getApiUrl(): string { @@ -30,11 +32,16 @@ function fetchGraphQL(query: string, variables: Record = {}): P const url = new URL(apiUrl); const body = JSON.stringify({ query, variables }); + // Use http for local/test endpoints (e.g. http://localhost:4000/graphql), + // https for production. Determined by the protocol in the configured URL. + const transport = url.protocol === "https:" ? https : http; + const defaultPort = url.protocol === "https:" ? 443 : 80; + return new Promise((resolve, reject) => { - const req = https.request( + const req = transport.request( { hostname: url.hostname, - port: url.port || 443, + port: url.port || defaultPort, path: url.pathname + url.search, method: "POST", headers: { diff --git a/lib/sitemap.ts b/lib/sitemap.ts index b8446729..779567b6 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -42,7 +42,11 @@ export const STATIC_ROUTES: Array> = [ // 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; +// override via SITEMAP_MIN_POSTS_PER_CATEGORY — set to 1 in .env.test so +// playwright fixtures (4 technology / 3 community posts) don't trigger the 503 fallback. +const MIN_POSTS_PER_CATEGORY = process.env.SITEMAP_MIN_POSTS_PER_CATEGORY + ? parseInt(process.env.SITEMAP_MIN_POSTS_PER_CATEGORY, 10) + : 5; // maps wordpress category data to the two supported frontend route namespaces. // matches by both slug and name (lowercased) to handle editorial inconsistencies. diff --git a/package-lock.json b/package-lock.json index 8306b6b6..6bd5da1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", + "server-only": "^0.0.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "typescript": "^4.7.4" @@ -5476,6 +5477,12 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", diff --git a/package.json b/package.json index 67e430fa..187f35b2 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", + "server-only": "^0.0.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "typescript": "^4.7.4" diff --git a/playwright.config.ts b/playwright.config.ts index 984b33fc..e2405251 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -117,6 +117,9 @@ export default defineConfig({ WORDPRESS_API_URL: GRAPHQL_API_URL, NEXT_PUBLIC_WORDPRESS_API_URL: GRAPHQL_API_URL, CRON_SECRET: 'test-secret', + // Playwright fixtures have fewer posts than production — lower the + // assertFullSitemap threshold so the ISR route returns 200, not 503. + SITEMAP_MIN_POSTS_PER_CATEGORY: '1', }, }, ], diff --git a/tests/e2e/Sitemap.spec.ts b/tests/e2e/Sitemap.spec.ts index 5c0e0590..710a5dea 100644 --- a/tests/e2e/Sitemap.spec.ts +++ b/tests/e2e/Sitemap.spec.ts @@ -65,7 +65,7 @@ test.describe('Sitemap ISR Route', () => { test('/sitemap.xml URLs are deduplicated', async ({ request, baseURL }) => { const response = await request.get(`${baseURL}/sitemap.xml`); const xml = await response.text(); - const locs = [...xml.matchAll(/(.*?)<\/loc>/g)].map(m => m[1]); + const locs = Array.from(xml.matchAll(/(.*?)<\/loc>/g)).map(m => m[1]); const unique = new Set(locs); expect(unique.size).toBe(locs.length); }); From 795b747f633309bab9c609cd453f7aed074b07f7 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 21:18:20 +0530 Subject: [PATCH 09/29] fix: harden tests and catch pre-push bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/mock-server.js: add AllPostsForSitemap handler before generic AllPosts guard, returning combined tech+community edges so assertFullSitemap sees posts in both categories (previously communityCount=0 caused 503 in every sitemap e2e test) - pages/api/cron/refresh-sitemap.ts: set Cache-Control: no-store explicitly in the handler so the test assertion is framework-independent (vercel.json headers are a Vercel platform layer, not applied by the local Next.js server) - lib/api-server.ts: replace allEdges=[...allEdges,...edges] in pagination loop with allEdges.push(...edges) to avoid O(n²) array allocation across pages - lib/sitemap.ts: guard parseInt result with Number.isNaN so an invalid SITEMAP_MIN_POSTS_PER_CATEGORY env var falls back to 5 instead of silently making the assertion unreachable (NaN < 5 === false) Co-Authored-By: Claude Sonnet 4.6 --- lib/api-server.ts | 2 +- lib/sitemap.ts | 7 +++---- pages/api/cron/refresh-sitemap.ts | 4 ++++ tests/mock-server.js | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/api-server.ts b/lib/api-server.ts index 1f731616..44932878 100644 --- a/lib/api-server.ts +++ b/lib/api-server.ts @@ -123,7 +123,7 @@ export async function getAllPostsForSitemap(): Promise<{ edges: any[] }> { ); const edges = data?.posts?.edges ?? []; - allEdges = [...allEdges, ...edges]; + allEdges.push(...edges); hasNextPage = data?.posts?.pageInfo?.hasNextPage ?? false; endCursor = data?.posts?.pageInfo?.endCursor ?? null; } diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 779567b6..41f10a0f 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -42,11 +42,10 @@ export const STATIC_ROUTES: Array> = [ // minimum posts required per category before the sitemap is considered trustworthy. // prevents a degraded partial wordpress response from replacing a good cached version. -// override via SITEMAP_MIN_POSTS_PER_CATEGORY — set to 1 in .env.test so +// override via SITEMAP_MIN_POSTS_PER_CATEGORY — set to 1 in playwright.config.ts so // playwright fixtures (4 technology / 3 community posts) don't trigger the 503 fallback. -const MIN_POSTS_PER_CATEGORY = process.env.SITEMAP_MIN_POSTS_PER_CATEGORY - ? parseInt(process.env.SITEMAP_MIN_POSTS_PER_CATEGORY, 10) - : 5; +const _parsedMin = parseInt(process.env.SITEMAP_MIN_POSTS_PER_CATEGORY ?? "", 10); +const MIN_POSTS_PER_CATEGORY = Number.isNaN(_parsedMin) ? 5 : _parsedMin; // maps wordpress category data to the two supported frontend route namespaces. // matches by both slug and name (lowercased) to handle editorial inconsistencies. diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index bf0b192e..3f3539a6 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -9,6 +9,10 @@ import { export const config = { maxDuration: 30 }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Cron responses must never be cached — prevent any proxy or CDN from + // serving a stale 401/200 on subsequent Vercel cron invocations. + res.setHeader("Cache-Control", "no-store"); + const expectedSecret = process.env.CRON_SECRET; // distinguish a deployment misconfiguration (500) from a wrong token (401). diff --git a/tests/mock-server.js b/tests/mock-server.js index 11fbb67b..fcf615a4 100644 --- a/tests/mock-server.js +++ b/tests/mock-server.js @@ -249,6 +249,22 @@ function handleGraphQL(body) { return { data: { post: { databaseId: 1, slug: 'mock-post', status: 'publish' } } }; } + // AllPostsForSitemap must be checked before the generic AllPosts guard below. + // It needs posts from both categories so assertFullSitemap() doesn't throw + // in the test environment (communityCount must be ≥ SITEMAP_MIN_POSTS_PER_CATEGORY). + if (query.includes('AllPostsForSitemap')) { + const techEdges = technologyPosts.data.posts.edges; + const commEdges = communityPosts.data.posts.edges; + return { + data: { + posts: { + edges: [...techEdges, ...commEdges], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }; + } + if (query.includes('AllPosts')) { return technologyPosts; } From c9db9f4cc8e96a84a39989a071886c30e5023ed3 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 21:31:29 +0530 Subject: [PATCH 10/29] feat: resolve copilot review#1, notfound page tests Signed-off-by: amaan-bhati --- app/not-found.tsx | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/app/not-found.tsx b/app/not-found.tsx index bfffffbc..46f112a8 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,23 +1,13 @@ -"use client"; -import { useEffect } from "react"; +import { redirect } from "next/navigation"; // 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. +// redirect('/404') issues a server-side HTTP redirect before any HTML is sent. +// Next.js automatically prepends basePath, so '/404' becomes '/blog/404' in production. +// This is faster and more reliable than a client-side window.location approach, +// and the tests see the actual 404 page content immediately after navigation. 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...

-
- ); + redirect("/404"); } From 177e778d0bb606b5839bfa156f5ffde2e2569a35 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Fri, 10 Apr 2026 21:41:14 +0530 Subject: [PATCH 11/29] feat: address copilotreviews#2, parseint validate, add request timeout Signed-off-by: amaan-bhati --- lib/api-server.ts | 9 +++++++++ lib/sitemap.ts | 8 ++++++-- pages/api/cron/refresh-sitemap.ts | 8 ++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/api-server.ts b/lib/api-server.ts index 44932878..759ba71b 100644 --- a/lib/api-server.ts +++ b/lib/api-server.ts @@ -37,6 +37,10 @@ function fetchGraphQL(query: string, variables: Record = {}): P const transport = url.protocol === "https:" ? https : http; const defaultPort = url.protocol === "https:" ? 443 : 80; + // 25s per page request — well under Vercel's 30s ISR timeout and leaves + // headroom for the pagination loop to complete across ~10 pages. + const TIMEOUT_MS = 25_000; + return new Promise((resolve, reject) => { const req = transport.request( { @@ -68,6 +72,11 @@ function fetchGraphQL(query: string, variables: Record = {}): P }); } ); + req.setTimeout(TIMEOUT_MS, () => { + req.destroy( + new Error(`GraphQL request timed out after ${TIMEOUT_MS}ms — WordPress may be slow or unreachable`) + ); + }); req.on("error", reject); req.write(body); req.end(); diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 41f10a0f..e76ecb37 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -44,8 +44,12 @@ export const STATIC_ROUTES: Array> = [ // prevents a degraded partial wordpress response from replacing a good cached version. // override via SITEMAP_MIN_POSTS_PER_CATEGORY — set to 1 in playwright.config.ts so // playwright fixtures (4 technology / 3 community posts) don't trigger the 503 fallback. -const _parsedMin = parseInt(process.env.SITEMAP_MIN_POSTS_PER_CATEGORY ?? "", 10); -const MIN_POSTS_PER_CATEGORY = Number.isNaN(_parsedMin) ? 5 : _parsedMin; +// Only accept explicit positive integers — rejects "", "0", "-1", "abc", "1.5". +// Falls back to 5 so a misconfigured env var cannot silently disable the guard. +const _rawMinPosts = process.env.SITEMAP_MIN_POSTS_PER_CATEGORY?.trim() ?? ""; +const _parsedMinPosts = /^\d+$/.test(_rawMinPosts) ? Number.parseInt(_rawMinPosts, 10) : NaN; +const MIN_POSTS_PER_CATEGORY = + Number.isFinite(_parsedMinPosts) && _parsedMinPosts > 0 ? _parsedMinPosts : 5; // maps wordpress category data to the two supported frontend route namespaces. // matches by both slug and name (lowercased) to handle editorial inconsistencies. diff --git a/pages/api/cron/refresh-sitemap.ts b/pages/api/cron/refresh-sitemap.ts index 3f3539a6..79d361d8 100644 --- a/pages/api/cron/refresh-sitemap.ts +++ b/pages/api/cron/refresh-sitemap.ts @@ -26,8 +26,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } - // auth is checked before method to avoid leaking valid HTTP methods to - // unauthenticated callers. vercel cron automatically injects this header. + // Auth is checked before method to avoid leaking valid HTTP methods to + // unauthenticated callers. + // + // Vercel's cron scheduler automatically adds "Authorization: Bearer " + // to every scheduled invocation — no manual header configuration is needed. + // See: https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs if (req.headers.authorization !== `Bearer ${expectedSecret}`) { return res.status(401).json({ ok: false, message: "Unauthorized" }); } From e4f1576771bd56bc3f7756631326f0c890ce97d4 Mon Sep 17 00:00:00 2001 From: amaan-bhati Date: Sat, 11 Apr 2026 00:42:05 +0530 Subject: [PATCH 12/29] feat: fix copilot review#2, fix redirect(/404) Signed-off-by: amaan-bhati --- app/not-found.tsx | 66 +++++++++++++++++++++++++++++++++------ tests/e2e/Sitemap.spec.ts | 11 +++++-- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/app/not-found.tsx b/app/not-found.tsx index 46f112a8..9f59adcf 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,13 +1,61 @@ -import { redirect } from "next/navigation"; - // 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. +// Without this file, any unmatched App Router path shows a blank +// "__next_error__" shell instead of a usable page. // -// redirect('/404') issues a server-side HTTP redirect before any HTML is sent. -// Next.js automatically prepends basePath, so '/404' becomes '/blog/404' in production. -// This is faster and more reliable than a client-side window.location approach, -// and the tests see the actual 404 page content immediately after navigation. +// IMPORTANT: Do NOT call redirect() here. redirect('/404') issues an RSC +// soft-navigation to /blog/404. Because /404 has no App Router page, the +// router calls this boundary again — infinite loop. Even a server-side +// HTTP redirect via redirect() inside not-found.tsx re-enters the App Router +// client on load, finds no route at /404, and loops. +// +// IMPORTANT: Do NOT render / here. app/layout.tsx already wraps +// every App Router page (including this boundary) with . +// Rendering them again produces invalid nested HTML that browsers silently +// repair by moving or dropping elements, making them invisible to Playwright. +// +// Instead, render a minimal but complete 404 page inline. All elements +// that NotFoundPage.spec.ts tests for are present: +// - text matching /404|not found/i +// -