-
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 all 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,156 @@ | ||
| import * as Sentry from "@sentry/nextjs"; | ||
|
|
||
| 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 warnOnMissingPathname(collection, doc) { | ||
| Sentry.logger.warn(`Collection item without \`pathname\` in sitemap`, { | ||
| collection, | ||
| slug: doc?.slug, | ||
| }); | ||
| } | ||
|
|
||
| async function getPagesEntries(api) { | ||
| const { docs } = await api.getCollection("pages", { | ||
| pagination: false, | ||
| select: { | ||
| pathname: true, | ||
| slug: true, | ||
| parent: true, | ||
| breadcrumbs: true, | ||
| updatedAt: true, | ||
| createdAt: true, | ||
| }, | ||
| where: { | ||
| and: [ | ||
| { | ||
| _status: { | ||
| equals: "published", | ||
| }, | ||
| }, | ||
| { | ||
| slug: { | ||
| not_in: ["404", "500"], | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| return docs | ||
| .map((doc) => { | ||
| if (!doc?.pathname) { | ||
| warnOnMissingPathname("pages", doc); | ||
| return null; | ||
| } | ||
|
|
||
| return toSitemapEntry(doc, doc.pathname); | ||
| }) | ||
| .filter(Boolean); | ||
| } | ||
|
|
||
| async function getOpportunitiesEntries(api) { | ||
| const { docs } = await api.getCollection("opportunities", { | ||
| pagination: false, | ||
| select: { | ||
| pathname: true, | ||
| slug: true, | ||
| type: true, | ||
| updatedAt: true, | ||
| createdAt: true, | ||
| date: true, | ||
| }, | ||
| }); | ||
|
|
||
| return docs | ||
| .map((doc) => { | ||
| if (!doc?.pathname) { | ||
| warnOnMissingPathname("opportunities", doc); | ||
| return null; | ||
| } | ||
|
|
||
| return toSitemapEntry(doc, doc.pathname); | ||
| }) | ||
| .filter(Boolean); | ||
| } | ||
|
|
||
| async function getSitemapEntries(api) { | ||
| const [pages, opportunities] = await Promise.all([ | ||
| getPagesEntries(api), | ||
| getOpportunitiesEntries(api), | ||
| ]); | ||
|
|
||
| return [...pages, ...opportunities].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>${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,30 @@ | ||
| import * as Sentry from "@sentry/nextjs"; | ||
|
|
||
| import { getSitemapXml } from "@/trustlab/lib/data"; | ||
|
|
||
| export async function getServerSideProps({ res }) { | ||
| 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.write(sitemapXml); | ||
| res.end(); | ||
| return { | ||
| props: {}, | ||
| }; | ||
| } catch (error) { | ||
| Sentry.captureException(error); | ||
| res.statusCode = 500; | ||
| res.end(); | ||
| return { | ||
| props: {}, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| export default function SitemapXml() { | ||
| return null; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.