-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add dynamic sitemap generation and API endpoint #1459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
1d92efd
ba59ab8
33e5ab7
b9373d0
4c4adca
189a448
2979891
29f6b10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import { formatPagePath } from "@commons-ui/payload"; | ||
|
|
||
| import { site } from "@/trustlab/utils"; | ||
|
|
||
| function normalizePathname(pathname) { | ||
| if (!pathname || typeof pathname !== "string") { | ||
| return null; | ||
| } | ||
|
|
||
| if (pathname === "/") { | ||
| return pathname; | ||
| } | ||
|
|
||
| const trimmed = pathname.trim(); | ||
| if (!trimmed) { | ||
| return null; | ||
| } | ||
|
|
||
| const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; | ||
| return withLeadingSlash.replace(/\/+$/, ""); | ||
| } | ||
|
|
||
| function getAbsoluteUrl(pathname) { | ||
| const normalizedPathname = normalizePathname(pathname); | ||
| if (!normalizedPathname) { | ||
| return null; | ||
| } | ||
|
|
||
| const siteUrl = site.url.replace(/\/+$/, ""); | ||
| return `${siteUrl}${normalizedPathname}`; | ||
| } | ||
|
|
||
| function getLastModified(doc) { | ||
| const rawDate = doc?.updatedAt || doc?.createdAt; | ||
| if (!rawDate) { | ||
| return null; | ||
| } | ||
|
|
||
| const parsedDate = new Date(rawDate); | ||
| if (Number.isNaN(parsedDate.getTime())) { | ||
| return null; | ||
| } | ||
|
|
||
| return parsedDate.toISOString(); | ||
| } | ||
|
|
||
| function toSitemapEntry(doc, pathname) { | ||
| const url = getAbsoluteUrl(pathname); | ||
| if (!url) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| url, | ||
| lastModified: getLastModified(doc), | ||
| }; | ||
| } | ||
|
|
||
| function getPagePathname(doc) { | ||
| if (!doc) { | ||
| return null; | ||
| } | ||
|
|
||
| const pathname = formatPagePath("pages", doc); | ||
| return normalizePathname(pathname); | ||
| } | ||
|
|
||
| async function getPagesEntries(api) { | ||
| const { docs } = await api.getCollection("pages", { | ||
|
kelvinkipruto marked this conversation as resolved.
|
||
| pagination: false, | ||
| select: { | ||
| slug: true, | ||
| parent: true, | ||
| breadcrumbs: true, | ||
| updatedAt: true, | ||
| createdAt: true, | ||
| }, | ||
| where: { | ||
| slug: { | ||
| not_in: ["404", "500"], | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| return docs | ||
| .map((doc) => toSitemapEntry(doc, getPagePathname(doc))) | ||
| .filter(Boolean); | ||
| } | ||
|
|
||
| function dedupeEntries(entries) { | ||
|
kelvinkipruto marked this conversation as resolved.
Outdated
|
||
| const seen = new Set(); | ||
|
|
||
| return entries.filter((entry) => { | ||
| if (seen.has(entry.url)) { | ||
| return false; | ||
| } | ||
|
|
||
| seen.add(entry.url); | ||
| return true; | ||
| }); | ||
| } | ||
|
|
||
| function escapeXml(value) { | ||
| return String(value) | ||
| .replaceAll("&", "&") | ||
| .replaceAll("<", "<") | ||
| .replaceAll(">", ">") | ||
| .replaceAll('"', """) | ||
| .replaceAll("'", "'"); | ||
| } | ||
|
|
||
| async function getSitemapEntries(api) { | ||
| const pages = await getPagesEntries(api); | ||
| return dedupeEntries(pages).sort((left, right) => | ||
| left.url.localeCompare(right.url), | ||
|
Comment on lines
+136
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Merging Useful? React with 👍 / 👎. |
||
| ); | ||
| } | ||
|
|
||
| async function buildSitemapXml(api) { | ||
| const entries = await getSitemapEntries(api); | ||
| const xmlEntries = entries | ||
| .map(({ url, lastModified }) => { | ||
| const lastModifiedNode = lastModified | ||
| ? `\n <lastmod>${lastModified}</lastmod>` | ||
| : ""; | ||
|
|
||
| return ` <url>\n <loc>${escapeXml(url)}</loc>${lastModifiedNode}\n </url>`; | ||
| }) | ||
| .join("\n"); | ||
|
|
||
| return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${xmlEntries}\n</urlset>\n`; | ||
|
kelvinkipruto marked this conversation as resolved.
|
||
| } | ||
|
|
||
| export default buildSitemapXml; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,8 @@ | ||
| export { getPageStaticPaths, getPageStaticProps, getRobotsTxt } from "./local"; | ||
| export { | ||
| getPageStaticPaths, | ||
| getPageStaticProps, | ||
| getRobotsTxt, | ||
| getSitemapXml, | ||
| } from "./local"; | ||
|
|
||
| export default undefined; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import * as Sentry from "@sentry/nextjs"; | ||
|
|
||
| import { getSitemapXml } from "@/trustlab/lib/data"; | ||
|
|
||
| export default async function handler(req, res) { | ||
| if (req.method !== "GET") { | ||
| res.setHeader("Allow", "GET"); | ||
| res.status(405).end(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const sitemapXml = await getSitemapXml(); | ||
| res.setHeader( | ||
| "Cache-Control", | ||
| "public, max-age=3600, stale-while-revalidate=86400", | ||
| ); | ||
| res.setHeader("Content-Type", "application/xml; charset=utf-8"); | ||
| res.send(sitemapXml); | ||
| } catch (error) { | ||
| Sentry.captureException(error); | ||
| res.status(500).end(); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.