All integrations

CleanJobData + Next.js

Combine Next.js with CleanJobData to build a fast, SEO-optimized job board.

Get Free API Key Deploy Next.js Starter to Vercel View GitHub Repo

Benefits

Setup Guide

1Create a server component

Fetch job listings in a Next.js server component.

const res = await fetch('https://api.cleanjobdata.com/jobs', {
  headers: { 'X-API-Key': process.env.CLEANJOBDATA_API_KEY }
});
const { data: jobs } = await res.json();

2Use ISR for performance

Add ISR with hourly revalidation for fast page loads with fresh data.

export const revalidate = 3600;

3Add SEO metadata

Generate unique title and description for every page.

export async function generateMetadata() {
  return { title: 'Software Engineer Jobs | My Board', description: '...' };
}

Advanced Recipes

Real-world patterns beyond the basic setup.

A complete, filterable listings page (app/jobs/page.tsx)

A full server component with TypeScript types, searchParams-driven filtering, and ISR — copy-pasteable, no missing imports. Adjust the field shape to match whichever `fields` you request from the API.

import { Metadata } from 'next';
import Link from 'next/link';

interface Job {
  id: string;
  title: string;
  company: { name: string };
  location: string;
  salary_min?: number;
  salary_max?: number;
  published: string;
}

interface PageProps {
  searchParams: Promise<{ title?: string; location?: string }>;
}

export async function generateMetadata({ searchParams }: PageProps): Promise<Metadata> {
  const { title } = await searchParams;
  const query = title || 'Software Engineer';
  return {
    title: `${query} Jobs | My Job Board`,
    description: `Browse the latest ${query} positions, updated hourly.`,
  };
}

async function getJobs(title?: string, location?: string): Promise<Job[]> {
  const params = new URLSearchParams({ limit: '20' });
  if (title) params.set('title', title);
  if (location) params.set('location', location);

  const res = await fetch(`https://api.cleanjobdata.com/jobs?${params.toString()}`, {
    headers: { 'X-API-Key': process.env.CLEANJOBDATA_API_KEY! },
    next: { revalidate: 3600 }, // ISR: revalidate hourly
  });

  if (!res.ok) throw new Error(`CleanJobData API ${res.status}`);
  const json = await res.json();
  return json.data as Job[];
}

export default async function JobsPage({ searchParams }: PageProps) {
  const { title, location } = await searchParams;
  const jobs = await getJobs(title, location);

  return (
    <main className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">Latest Job Listings</h1>

      <form className="flex gap-2 mb-8">
        <input name="title" defaultValue={title} placeholder="Job title or skill..." className="border p-2 rounded w-full" />
        <input name="location" defaultValue={location} placeholder="City, state, or country" className="border p-2 rounded w-full" />
        <button type="submit" className="bg-black text-white px-4 py-2 rounded">Search</button>
      </form>

      <div className="space-y-4">
        {jobs.map((job) => (
          <div key={job.id} className="border p-4 rounded hover:border-black transition">
            <h2 className="text-xl font-semibold">{job.title}</h2>
            <p className="text-gray-600">{job.company.name} • {job.location}</p>
            <Link href={`/jobs/${job.id}`} className="text-blue-600 mt-2 inline-block">View Details →</Link>
          </div>
        ))}
      </div>
    </main>
  );
}

Dynamic sitemap for job SEO (app/sitemap.ts)

Job pages are transient — a static sitemap goes stale within days. Next.js's sitemap.ts route regenerates it from live data on a schedule, which is exactly what the starter repo's own app/sitemap.ts does.

import { MetadataRoute } from 'next';

interface JobSitemapEntry {
  id: string;
  published: string;
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const res = await fetch('https://api.cleanjobdata.com/jobs?limit=500&fields=id,published', {
    headers: { 'X-API-Key': process.env.CLEANJOBDATA_API_KEY! },
    next: { revalidate: 86400 }, // rebuild once a day
  });
  const { data: jobs } = (await res.json()) as { data: JobSitemapEntry[] };

  const jobUrls = jobs.map((job) => ({
    url: `https://myjobboard.com/jobs/${job.id}`,
    lastModified: new Date(job.published),
  }));

  return [
    { url: 'https://myjobboard.com', lastModified: new Date() },
    { url: 'https://myjobboard.com/jobs', lastModified: new Date() },
    ...jobUrls,
  ];
}

Environment variables

Set your key locally before running the starter or your own integration.

# .env.local
CLEANJOBDATA_API_KEY=cjd_live_your_api_key_here

Frequently Asked Questions

Can I use CleanJobData with Next.js App Router?

Yes. Call the API directly from a Server Component or Route Handler so your key never reaches the client: `const res = await fetch('https://api.cleanjobdata.com/jobs?title=engineer&remote=true', { headers: { 'X-API-Key': process.env.CLEANJOBDATA_API_KEY }, next: { revalidate: 3600 } })`. Pages Router works the same way from getServerSideProps or an API route — the API is plain REST, so nothing here is App Router-specific.

Does CleanJobData work with ISR?

Yes, and it's the standard pattern for a job board built on this API. Set `revalidate` on the fetch call (or `export const revalidate = 3600` on the route segment) to control how stale a page can get. An hour matches how often most listings actually change; drop it to a few hundred seconds for pages that need near-live data, and watch the `X-RateLimit-Remaining` response header if you tighten it further.

How do I handle client-side search without leaking my API key?

Add a Route Handler that proxies the request server-side: `app/api/jobs/route.ts` reads `searchParams` off the incoming request, calls CleanJobData with `process.env.CLEANJOBDATA_API_KEY` (never exposed to the browser), and returns the JSON. Your client component then calls `/api/jobs?...` — the same-origin route — instead of the CleanJobData API directly, so the key never appears in browser network traffic or bundled JS.

What happens if I hit the CleanJobData rate limit?

If pages are served through ISR (`next: { revalidate: 3600 }` or a route segment's `revalidate` export), Next.js keeps serving the last successfully cached page to visitors even if a background revalidation fetch fails or gets rate-limited — users don't see an error, and the next successful revalidation catches the page back up. This only holds for ISR'd pages; a Route Handler proxying live, uncached requests (like the search-widget pattern above) will surface the rate-limit error directly, so add basic retry/backoff there if you expect bursty traffic.