Salaries
Build indexable salary hubs and title, skill, company, and location detail pages.
A
JBuild salary pages from Cavuno’s aggregate read models. Salary data is organized by title, skill, company, and location, with cross-axis methods for combinations such as a title in a location.
Prerequisites
- A shared public
boardclient. - Routes under
/salariesthat use the SDK path helpers. - Enough real salary observations to make each page useful. There is no
features.salariesflag; empty data is the gate.
Load the salary hubs
12345678910111213141516171819202122232425262728293031323334// src/data/salaries.tsimport {salaryLocationPath,salarySkillPath,salaryTitlePath,} from '@cavuno/board/paths';import { board } from '../lib/board';export async function loadSalaryHubs() {const { language } = await board.context();const [companies, titles, skills, locations] = await Promise.all([board.salaries.companies.list(),board.salaries.titles.list({ locale: language }),board.salaries.skills.list({ locale: language }),board.salaries.locations.list({ locale: language }),]);return {companies: companies.data,titles: titles.data.map((item) => ({...item,path: salaryTitlePath(item.slug),})),skills: skills.data.map((item) => ({...item,path: salarySkillPath(item.slug),})),locations: locations.data.map((item) => ({...item,path: salaryLocationPath(item.placeSlug),})),};}
Passing board.context().language asks the API for board-language names and canonical slugs. Do not translate slugs in the browser.
Load a canonical detail page
Each detail response includes source and canonical slugs. Redirect when the inbound slug differs from the returned canonical value.
1234567891011121314export async function loadTitleSalary(inboundSlug: string) {const { language } = await board.context();const detail = await board.salaries.titles.retrieve(inboundSlug, {locale: language,});return {detail,redirectTo:inboundSlug === detail.canonicalSlug? null: salaryTitlePath(detail.canonicalSlug),};}
The same pattern applies to skills and locations. Use board.companies.salaries(companySlug) for a company overview and .category(companySlug, categorySlug, { locale }) for one company/category combination.
Expected result
Hub loaders return only the salary entities Cavuno can support with real aggregates. Detail loaders return localized names, canonical slugs, currency, sample counts, and nullable aggregate sections.
Verify the result
- Empty hub lists do not produce empty indexable routes.
- Localized pages use the canonical slugs returned by the API.
- A legacy or source-language slug returns a permanent redirect to the canonical page.
- Currency, minimum, maximum, and sample counts come from the response rather than local estimates.
Errors and edge cases
- Unknown salary slugs can return 404. Convert only that typed error into your framework’s not-found response.
- Some aggregate fields are nullable when the data cannot support a calculation.
- Location indexes are a flattened hierarchy. Rebuild parent/child navigation from
parentSlugrather than guessing from names. - Cross-axis pages are intentionally not emitted by the current sitemap walker because the API lacks a bulk pairs endpoint. Keep them discoverable through internal links.
Production checklist
- Format currencies with the board locale and the returned currency.
- Include visible sample counts so readers understand the strength of an aggregate.
- Do not index thin or empty pages.
- Add canonical metadata and the appropriate salary JSON-LD helpers from
@cavuno/board/seo.
Next, add signed-in candidate experiences with Candidate accounts and talent.