Jobs and search
Build paginated job discovery, faceted search, canonical details, and related jobs.
A
JBuild the core public experience: a filterable jobs page, a canonical job detail page, and a related-jobs rail. The API returns slim PublicJobCard objects for lists and a full PublicJob for details.
Prerequisites
- A shared public
boardclient. - A framework route for
/jobs. - A detail route matching
/companies/:companySlug/jobs/:jobSlug.
These methods are public. Cache them for a short, explicit period if your framework supports it; do not reuse that policy for board.me methods.
Load a jobs page
Convert untrusted URL parameters with the SDK filter parser, then pass supported values to board.jobs.list.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// src/data/jobs.tsimport { isNotFound } from '@cavuno/board';import { parseListingFilters } from '@cavuno/board/filters';import { jobDetailPath } from '@cavuno/board/paths';import { board } from '../lib/board';export async function loadJobsPage(search: Record<string, unknown>) {const filters = parseListingFilters(search);return board.jobs.list({limit: 20,remoteOption: filters.remoteOption? [filters.remoteOption]: undefined,employmentType: filters.employmentType? [filters.employmentType]: undefined,seniority: filters.seniority,sort: filters.sort,},{ cache: 'force-cache' },);}export async function searchJobs(query: string) {return board.jobs.search({ query, limit: 20 },undefined,{ cache: 'no-store' },);}export async function loadJob(jobSlug: string) {try {const job = await board.jobs.retrieve(jobSlug);const related = await board.jobs.similar(jobSlug, { limit: 5 });const canonicalPath =job.company?.slug && job.slug? jobDetailPath(job.company.slug, job.slug): null;return { job, related: related.data, canonicalPath };} catch (error) {if (isNotFound(error)) return null;throw error;}}
list and search return an envelope with data, hasMore, and nextCursor. Job catalog responses can also include count, limit, offset, and gatedCount. Render the server values instead of calculating a total from the current page.
Use nextCursor to request the next page:
12345const first = await board.jobs.list({ limit: 20 });const second = first.nextCursor? await board.jobs.list({ limit: 20, cursor: first.nextCursor }): null;
Expected result
The listing loader returns a page of PublicJobCard objects and cursor state. The detail loader returns one full PublicJob, up to five related cards, and either a canonical path or null when the resource is missing.
Verify the result
Open /jobs and confirm that:
- Every card uses
job.slugandjob.company?.slugto construct its detail URL. - Changing a supported filter changes the API request and the visible results.
- The next-page control disappears when
hasMoreisfalse. - An unknown job slug produces your 404 page, not an empty job template.
Errors and edge cases
limitmust be between 1 and 100. Radius searches accept 10–250 km.- Category and skill listing slugs can return 404 when they cannot be resolved.
fields: '+description'is the only supported sparse-field addition for cards.- A job without both a job slug and company slug has no canonical detail path. Do not invent one.
- Search is a
POST; avoid placing it in a long-lived shared cache unless the cache key includes the entire body.
Production checklist
- Keep one canonical detail route:
/companies/:companySlug/jobs/:jobSlug. - Abort requests tied to navigation or rapidly changing search input.
- Log
BoardApiError.requestIdfor unexpected failures. - Add metadata and JobPosting JSON-LD with SEO and structured data.
Next, connect each job to its employer with Companies.