How to build job detail and application pages

Render a canonical job page, similar jobs, native applications, and framework-native not-found behavior.

This recipe turns a job slug into an indexable detail page and a complete application path. Your framework owns the route and 404 response. Cavuno owns the job record, application rules, uploads, and duplicate-application state.

Use the canonical route

Declare /companies/:companySlug/jobs/:jobSlug. Retrieve by job slug, then redirect a stale company segment to the canonical path supplied by the job.

ts
import { isNotFound } from '@cavuno/board';
export async function loadJob(companySlug: string, jobSlug: string) {
try {
const job = await board.jobs.retrieve(jobSlug);
if (job.company.slug !== companySlug) {
return { redirect: `/companies/${job.company.slug}/jobs/${job.slug}` };
}
return { job };
} catch (error) {
if (isNotFound(error)) return { notFound: true };
throw error;
}
}

At the route boundary, turn notFound: true into the framework’s native 404—notFound() in Next.js, a thrown 404 Response in React Router, or a 404 Response in Astro. Do not return a 200 page that merely says “not found”.

Build the page in parallel

ts
const [job, similar] = await Promise.all([
board.jobs.retrieve(jobSlug),
board.jobs.similar(jobSlug, { limit: 6 }),
]);

Render the job’s title, company, locations, employment details, salary, description, and application call to action. Use the SEO helper for metadata and JobPosting JSON-LD; render the JSON-LD in the initial server HTML.

Only emit JobPosting when the same page visibly contains one complete, active job and a working way to apply. The visible description and structured description must represent the full role, and an expiring job needs an accurate validThrough. Test the initial HTML with Cavuno’s JobPosting schema validator and Google’s Rich Results Test.

Choose the application path

External jobs send the visitor to the returned external application URL. Native applications use board.jobs.apply. Create the application first, then attach a résumé to that application. Repeating apply returns the existing application rather than creating a duplicate.

ts
const options = {
cache: 'no-store' as const,
headers: { authorization: `Bearer ${accessToken}` },
};
const application = await board.jobs.apply(
job.slug,
{
email,
name,
coverNote,
},
options,
);
if (file) {
await board.jobs.uploadApplicationResume(
job.slug,
file,
signedIn ? undefined : { applicationId: application.id },
options,
);
}

Treat validation errors as field feedback. Treat an existing application as a state to show, not a generic failure. A signed-in candidate can query board.jobs.myApplication(job.slug) and link to the application dashboard.

Use a visible <label> for every input, associate field errors with their controls, and place an error summary before the form with links to each invalid field. Move focus to that summary after a failed submission and announce the success state. State accepted résumé types, the size limit, whether the file is retained, and how the applicant can request deletion. The W3C forms tutorial shows the expected labeling and notification patterns.

Remove expired jobs correctly

At the public SDK boundary, an unavailable job is a not-found response. Return 404 or 410, remove it from internal links and sitemaps, and send a removal notification through the Google Indexing API. If your application deliberately retains an expired job page from another source, remove JobPosting, disable the apply action, and ensure validThrough is in the past. Do not leave active job markup on an expired page.

Keep the two cache classes separate

The public job and similar-job reads may use a shared revalidation policy. Application state, uploads, and submissions are private and must use no-store. Never let a cached page serialize an access token or a candidate’s application status.

Verify the job journey

  1. Open the canonical URL and inspect the raw HTML for the title, canonical link, and JobPosting JSON-LD.
  2. Open the same job under the wrong company slug and confirm a permanent canonical redirect.
  3. Open an expired or unknown job slug and confirm a real 404 or 410 response with no JobPosting; if you deliberately retain expired pages, confirm the alternative policy above instead.
  4. Submit valid and invalid native applications and confirm the correct success and field-error states.
  5. Sign in as two candidates and confirm application state never crosses sessions.
  6. Complete the application with only a keyboard and confirm errors and success are announced without losing entered data.

Next, add company directory and profile pages so every job links into a useful employer entity.