Add job URLs to a static sitemap
If your site’s sitemap is generated at build time and you want the job pages inside it, pull the URLs from the Build-time feed with fields=minimal. Each item is { slug, url, updated_at }: the absolute page URL and its last-modified time.
Fetching every URL
Section titled “Fetching every URL”Follow the cursor until has_more is false. With limit=200 most businesses fit in one page.
curl "https://app.simplifylocal.app/api/businesses/{slug}/feed/jobs?fields=minimal&limit=200" \ -H "Authorization: Bearer sl_live_…"{ "items": [ { "slug": "house-washing-marietta", "url": "https://example.com/work/house-washing-marietta", "updated_at": "2026-04-13T09:02:11Z" } ], "pagination": { "next_cursor": null, "has_more": false }}A shared fetcher, in TypeScript:
type FeedItem = { slug: string; url: string; updated_at: string };
export async function fetchJobUrls(slug: string, key: string): Promise<FeedItem[]> { const items: FeedItem[] = []; let cursor: string | null = null; do { const url = new URL(`https://app.simplifylocal.app/api/businesses/${slug}/feed/jobs`); url.searchParams.set("fields", "minimal"); url.searchParams.set("limit", "200"); if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); if (!res.ok) throw new Error(`feed ${res.status}: ${(await res.json()).error}`); const page = await res.json(); items.push(...page.items); cursor = page.pagination.next_cursor; } while (cursor); return items;}With @astrojs/sitemap, pass the URLs through customPages in astro.config.mjs. The integration takes plain URL strings, so lastmod is not carried; if you need it, write the sitemap yourself with an endpoint as below.
import { defineConfig } from "astro/config";import sitemap from "@astrojs/sitemap";import { fetchJobUrls } from "./src/lib/simplifylocal.ts";
const jobs = await fetchJobUrls(process.env.SL_BUSINESS_SLUG, process.env.SL_API_KEY);
export default defineConfig({ site: "https://example.com", integrations: [sitemap({ customPages: jobs.map((j) => j.url) })],});To keep lastmod, emit a second sitemap from an endpoint, src/pages/sitemap-jobs.xml.ts, and list it in your sitemap index:
import type { APIRoute } from "astro";import { fetchJobUrls } from "../lib/simplifylocal";
export const GET: APIRoute = async () => { const jobs = await fetchJobUrls(import.meta.env.SL_BUSINESS_SLUG, import.meta.env.SL_API_KEY); const body = `<?xml version="1.0" encoding="UTF-8"?>\n` + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` + jobs.map((j) => ` <url><loc>${j.url}</loc><lastmod>${j.updated_at}</lastmod></url>`).join("\n") + `\n</urlset>\n`; return new Response(body, { headers: { "Content-Type": "application/xml" } });};Hugo can fetch remote JSON at build time with resources.GetRemote. Add the job URLs to a sitemap template override at layouts/sitemap.xml:
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {{ range .Data.Pages }} <url> <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }} <lastmod>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" }}</lastmod>{{ end }} </url> {{ end }} {{ $url := printf "https://app.simplifylocal.app/api/businesses/%s/feed/jobs?fields=minimal&limit=200" (getenv "SL_BUSINESS_SLUG") }} {{ $opts := dict "headers" (dict "Authorization" (printf "Bearer %s" (getenv "SL_API_KEY"))) }} {{ with resources.GetRemote $url $opts }} {{ with .Err }}{{ errorf "SimplifyLocal feed: %s" . }}{{ end }} {{ range (unmarshal .Content).items }} <url> <loc>{{ .url }}</loc> <lastmod>{{ .updated_at }}</lastmod> </url> {{ end }} {{ end }}</urlset>This fetches one page of up to 200 jobs. For more, loop on pagination.next_cursor in a small build script and write the result to data/, then range over site.Data in the template.
Rebuilding when a job changes
Section titled “Rebuilding when a job changes”A static sitemap is only as fresh as the last build. Trigger a rebuild from a webhook on job.published and job.unpublished, or rebuild on a schedule. The feed may be cached for up to 10 minutes, so a build that starts the moment the webhook lands can see the previous state; delay the trigger by a few minutes or rebuild twice.