Skip to content

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.

Follow the cursor until has_more is false. With limit=200 most businesses fit in one page.

Terminal window
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.

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.