Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { MetadataRoute } from 'next'

const WP_API_URL = process.env.NEXT_PUBLIC_WORDPRESS_API_URL || 'https://wp.keploy.io/graphql'

Comment thread
amaan-bhati marked this conversation as resolved.
interface WPPostNode {
slug: string
modified: string
categories?: {
nodes: {
slug: string
}[]
}
}

async function fetchGraphQL<T>(query: string, variables: Record<string, any> = {}): Promise<T | null> {
try {
const res = await fetch(WP_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
next: { revalidate: 86400 }, // 24-hour cache
})

if (!res.ok) {
console.error("fetchGraphQL res not ok:", res.status, res.statusText)
return null
}
const json = await res.json()
return json.data as T
} catch (error) {
console.error('WPGraphQL fetch error:', error)
Comment thread
amaan-bhati marked this conversation as resolved.
Comment on lines +27 to +33
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchGraphQL ignores GraphQL-level errors (json.errors) and will return json.data even when the response contains errors or partial data. That can silently generate an incomplete/incorrect sitemap; consider checking json.errors (similar to lib/api.ts:25-28) and failing fast or returning null with an actionable error message.

Suggested change
console.error("fetchGraphQL res not ok:", res.status, res.statusText)
return null
}
const json = await res.json()
return json.data as T
} catch (error) {
console.error('WPGraphQL fetch error:', error)
console.error(
'fetchGraphQL HTTP error:',
res.status,
res.statusText,
'- please verify the WPGraphQL endpoint URL and check server logs for more details.'
)
return null
}
const json = await res.json()
if (json && Array.isArray(json.errors) && json.errors.length > 0) {
console.error(
'fetchGraphQL GraphQL error(s) returned from WPGraphQL. Inspect the query and server logs for details:',
json.errors
)
return null
}
return json.data as T
} catch (error) {
console.error(
'WPGraphQL fetch error. Please check network connectivity, the WPGraphQL endpoint configuration, and server logs:',
error
)

Copilot uses AI. Check for mistakes.
return null
Comment on lines +26 to +34
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These error logs don’t provide a clear next step, and they’ll appear in production if WPGraphQL is transiently unavailable (e.g., rate limiting). Consider improving the message to include actionable guidance (e.g., check WPGraphQL endpoint/env vars, retry later) and/or handling retries/backoff; also consider avoiding logging full errors repeatedly during ISR to reduce log noise.

Copilot generated this review using guidance from organization custom instructions.
}
}

async function fetchAllPosts(): Promise<WPPostNode[]> {
const allPosts: WPPostNode[] = []
let hasNextPage = true
let after = ''

while (hasNextPage) {
const query = `
query AllPosts($after: String) {
posts(first: 100, after: $after) {
edges {
node {
slug
modified
categories {
nodes {
slug
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`
const data = await fetchGraphQL<any>(query, { after: after || null })
if (!data?.posts) break

allPosts.push(...data.posts.edges.map((edge: any) => edge.node))
hasNextPage = data.posts.pageInfo.hasNextPage
after = data.posts.pageInfo.endCursor
}
return allPosts
}

async function fetchAllTaxonomies(type: 'tags' | 'categories' | 'users'): Promise<{ slug: string; lastModified: Date }[]> {
const allNodes: { slug: string; lastModified: Date }[] = []
let hasNextPage = true
let after = ''

while (hasNextPage) {
const query = `
query All${type}($after: String) {
${type}(first: 100, after: $after) {
edges {
node {
slug
posts(first: 1) {
nodes {
modified
}
}
}
Comment on lines +86 to +92
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchAllTaxonomies uses posts(first: 1) without an explicit orderby. This makes lastModified potentially incorrect (often returns most recent by DATE, not by MODIFIED), which undermines the goal of accurate <lastmod> values. Add an explicit order (e.g., order by MODIFIED DESC) to ensure the returned modified is actually the latest for that taxonomy/author.

Copilot uses AI. Check for mistakes.
}
pageInfo { hasNextPage endCursor }
}
}
`
const data = await fetchGraphQL<any>(query, { after: after || null })
if (!data?.[type]) {
if (process.env.NODE_ENV === 'development') {
console.debug(`fetchGraphQL returned missing data for ${type}`)
}
break
}

if (process.env.NODE_ENV === 'development') {
console.debug(`Fetched ${type} page with ${data[type].edges.length} items. hasNextPage: ${data[type].pageInfo.hasNextPage}, endCursor: ${data[type].pageInfo.endCursor}`)
}

// Failsafe to prevent excessive polling
if (allNodes.length > 5000) {
if (process.env.NODE_ENV === 'development') {
console.debug(`Failsafe triggered for ${type}`)
}
break
}
Comment on lines +99 to +116
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unconditional console.log “DEBUG:” statements in the sitemap generator. This will produce noisy logs during builds/ISR and doesn’t follow the logging guideline here (debug logs should be gated, and error logs should include a clear next step). Please remove these logs or gate them behind an explicit debug flag (e.g., an env var) and keep production output minimal/actionable.

Copilot generated this review using guidance from organization custom instructions.

for (const edge of data[type].edges) {
const node = edge.node
const postMod = node.posts?.nodes?.[0]?.modified
// Only include taxonomies that actually have published posts
if (postMod) {
allNodes.push({ slug: node.slug, lastModified: new Date(postMod) })
}
}
hasNextPage = data[type].pageInfo.hasNextPage
after = data[type].pageInfo.endCursor
}
return allNodes
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://keploy.io/blog'

// Sequential fetching to deeply respect WP Engine GraphQL burst limits
const posts = await fetchAllPosts()
Comment on lines +132 to +136
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new sitemap endpoint is SEO-critical but isn’t covered by existing e2e tests. Consider adding a Playwright test that requests /blog/sitemap.xml, asserts a 200 + application/xml content-type, and verifies it contains a few expected URLs.

Copilot uses AI. Check for mistakes.
const tags = await fetchAllTaxonomies('tags')
const categories = await fetchAllTaxonomies('categories')
const authors = await fetchAllTaxonomies('users')

Comment on lines +132 to +140
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are Playwright E2E tests for SEO, but none cover /blog/sitemap.xml. Adding a basic test that requests the sitemap and asserts it returns XML with a few expected URLs (and no obvious 404 targets) would help prevent regressions in sitemap generation and routing alignment.

Copilot uses AI. Check for mistakes.
// Get global latest from posts for the root `/blog`
let globalLatestModified = new Date(0)
posts.forEach(post => {
const postDate = new Date(post.modified)
if (postDate > globalLatestModified) {
globalLatestModified = postDate
}
})
if (globalLatestModified.getTime() === 0) {
globalLatestModified = new Date()
}

// Static root blog entry using global max post modification date
const sitemapData: MetadataRoute.Sitemap = [
{
url: `${baseUrl}`,
lastModified: globalLatestModified,
changeFrequency: 'daily',
priority: 1.0,
}
]

// Process Tags
tags.forEach(tag => {
sitemapData.push({
url: `${baseUrl}/tag/${encodeURIComponent(tag.slug)}`,
lastModified: tag.lastModified,
changeFrequency: 'weekly',
priority: 0.64,
})
})
Comment on lines +164 to +171
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sitemap tag URLs are built from tag.slug, but the site’s tag routes/links appear to use the tag name (e.g. components/tag.tsx links to /tag/${name}, and the previous sitemap contained URLs like /tag/Feature%20Flags). Using slug here may create URLs that are inconsistent with internal linking (and can cause duplicate/incorrect indexing). Consider generating tag URLs using the same value the route expects (or update routing to consistently use slugs).

Copilot uses AI. Check for mistakes.

// Process Categories
categories.forEach(cat => {
sitemapData.push({
url: `${baseUrl}/${encodeURIComponent(cat.slug)}`,
lastModified: cat.lastModified,
changeFrequency: 'weekly',
priority: 0.80,
})
Comment on lines +173 to +180
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sitemap currently emits URLs for all WP categories (${baseUrl}/${cat.slug}), but the Next.js routes in this repo only support /technology and /community (no generic category route). If WP ever contains other categories with posts, this will publish invalid URLs to crawlers. Consider filtering categories (and post primaryCategory) to the set of routes the frontend actually serves, or mapping WP categories to supported route segments.

Copilot uses AI. Check for mistakes.
})

// Process Authors (verified exact path: /blog/authors/[slug])
authors.forEach(author => {
sitemapData.push({
url: `${baseUrl}/authors/${encodeURIComponent(author.slug)}`,
lastModified: author.lastModified,
changeFrequency: 'weekly',
priority: 0.64,
})
Comment on lines +183 to +190
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author sitemap entries are built from WPGraphQL users.node.slug (/authors/${slug}), but the existing author route (pages/authors/[slug].tsx) generates/looks up slugs via sanitizeAuthorSlug(ppmaAuthorName) derived from posts. These formats likely won’t match, leading to sitemap URLs that 404. Consider deriving author slugs the same way the route does (from ppmaAuthorName) or reusing sanitizeAuthorSlug when building author URLs.

Copilot uses AI. Check for mistakes.
})
Comment on lines +183 to +191
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author URLs in the sitemap are built from WP user slug, but the site’s author routing uses sanitizeAuthorSlug(ppmaAuthorName) (see components/AuthorMapping.tsx / pages/authors/[slug].tsx). This mismatch can yield non-canonical or even 404 author URLs in the sitemap. Consider deriving author slugs using sanitizeAuthorSlug (e.g., by aggregating author names from posts) or by querying name and sanitizing it instead of using WP slug directly.

Copilot uses AI. Check for mistakes.

const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)

// Track unique URLs to ensure no duplicates
const addedUrls = new Set<string>()
sitemapData.forEach(item => addedUrls.add(item.url))

// Process Posts
posts.forEach((post) => {
let primaryCategory = post.categories?.nodes[0]?.slug
if (!primaryCategory || primaryCategory === 'uncategorized') {
// Skip posts that aren't properly categorized to avoid providing invalid URLs with the wrong metadata/headers in the sitemap.
return
}
Comment thread
amaan-bhati marked this conversation as resolved.

const url = `${baseUrl}/${encodeURIComponent(primaryCategory)}/${encodeURIComponent(post.slug)}`

if (addedUrls.has(url)) return
addedUrls.add(url)

const postModifiedDate = new Date(post.modified)
Comment thread
amaan-bhati marked this conversation as resolved.
const priority = postModifiedDate >= thirtyDaysAgo ? 0.8 : 0.5

sitemapData.push({
url,
lastModified: postModifiedDate,
changeFrequency: 'daily',
priority,
})
})

return sitemapData
}
Loading
Loading