@cavuno/board 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +433 -0
- package/README.md +6 -2
- package/dist/{_spec-nkWAraS0.d.mts → _spec-BZDv2dc5.d.mts} +100 -68
- package/dist/{_spec-nkWAraS0.d.ts → _spec-BZDv2dc5.d.ts} +100 -68
- package/dist/bin.mjs +7 -2
- package/dist/{blog-iV9XEIGf.d.ts → blog-BHx6iH3Z.d.ts} +1 -1
- package/dist/{blog-DGBHmExV.d.mts → blog-DyDvyQrI.d.mts} +1 -1
- package/dist/{board-JhdUWIoq.d.ts → board-BF3Qao6J.d.mts} +7 -6
- package/dist/{board-BJbXd_9_.d.mts → board-BTEZ0uiI.d.ts} +7 -6
- package/dist/breadcrumbs-SGaWth6C.d.ts +372 -0
- package/dist/breadcrumbs-icxRVydh.d.mts +372 -0
- package/dist/chunk-BGKNXG4H.mjs +146 -0
- package/dist/{chunk-NXY3OWGF.mjs → chunk-T2KC6XGV.mjs} +1 -1
- package/dist/{chunk-STE77MAT.mjs → chunk-YEAQ3JEI.mjs} +143 -134
- package/dist/doctor.js +1 -1
- package/dist/doctor.mjs +1 -1
- package/dist/filters.d.mts +25 -20
- package/dist/filters.d.ts +25 -20
- package/dist/filters.js +3 -692
- package/dist/filters.mjs +3 -29
- package/dist/format.d.mts +57 -231
- package/dist/format.d.ts +57 -231
- package/dist/format.js +327 -968
- package/dist/format.mjs +207 -264
- package/dist/go.js +11 -1
- package/dist/go.mjs +1 -1
- package/dist/index.d.mts +67 -76
- package/dist/index.d.ts +67 -76
- package/dist/index.js +45 -36
- package/dist/index.mjs +45 -36
- package/dist/{jobs-BiEvL9d7.d.ts → jobs-BX6d_sdd.d.mts} +4 -4
- package/dist/{jobs-lVvKCBCU.d.mts → jobs-BbLJfz1z.d.ts} +4 -4
- package/dist/paths.d.mts +89 -1
- package/dist/paths.d.ts +89 -1
- package/dist/paths.js +62 -14
- package/dist/paths.mjs +11 -3
- package/dist/route-contract.js +23 -13
- package/dist/route-contract.mjs +2 -2
- package/dist/{search-CCyFWPi3.d.mts → search-7B-mVRNA.d.ts} +14 -3
- package/dist/{search-CyRvVMUp.d.ts → search-OKrdAHv8.d.mts} +14 -3
- package/dist/seo.d.mts +30 -29
- package/dist/seo.d.ts +30 -29
- package/dist/seo.js +197 -718
- package/dist/seo.mjs +26 -24
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/sitemap.js +25 -14
- package/dist/sitemap.mjs +3 -2
- package/dist/skills.js +1 -1
- package/dist/skills.mjs +1 -1
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/dist/suggest.js +17 -3
- package/dist/suggest.mjs +17 -3
- package/dist/well-known.js +23 -13
- package/dist/well-known.mjs +2 -2
- package/package.json +2 -4
- package/skills/cavuno-board-api-client/SKILL.md +42 -0
- package/skills/cavuno-board-applications/SKILL.md +4 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -12
- package/skills/cavuno-board-format/SKILL.md +68 -56
- package/skills/cavuno-board-i18n/SKILL.md +102 -21
- package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
- package/skills/cavuno-board-seo/BLOG.md +4 -1
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
- package/skills/cavuno-board-seo/LISTING.md +35 -12
- package/skills/cavuno-board-seo/SALARY.md +83 -18
- package/skills/cavuno-board-seo/SKILL.md +3 -2
- package/skills/manifest.json +3 -3
- package/dist/breadcrumbs-1hMjcsqA.d.mts +0 -171
- package/dist/breadcrumbs-DexU07TT.d.ts +0 -171
- package/dist/chunk-IJCJ26SQ.mjs +0 -596
- package/dist/chunk-L7ZOYWHW.mjs +0 -98
- package/dist/chunk-LUGJWOT7.mjs +0 -96
- package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
- package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-format
|
|
3
|
-
description: Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates,
|
|
3
|
+
description: Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates, custom-field display, or salary-stat numbers.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Display formatting
|
|
7
7
|
|
|
8
|
-
`@cavuno/board/format` matches the hosted board's display rules
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`@cavuno/board/format` matches the hosted board's display rules for
|
|
9
|
+
**contract and data-shaping** helpers. Chrome words (enum→label, uiCopy,
|
|
10
|
+
salary lexicon) are application-owned and are not exported here.
|
|
11
|
+
|
|
12
|
+
Read the board language once and pass it as the required first argument to
|
|
13
|
+
every label-producing helper.
|
|
11
14
|
|
|
12
15
|
Filter controls use `cavuno-board-filters`. Salary values remain in their wire
|
|
13
16
|
currency and retain their server-computed amounts.
|
|
@@ -16,87 +19,96 @@ currency and retain their server-computed amounts.
|
|
|
16
19
|
|
|
17
20
|
```ts snippet
|
|
18
21
|
import {
|
|
19
|
-
fieldLabel,
|
|
20
22
|
formatDate,
|
|
21
23
|
formatPublishedRelativeDate,
|
|
22
24
|
formatSalaryRange,
|
|
25
|
+
formatSalaryStat,
|
|
26
|
+
formatSalaryStatRange,
|
|
27
|
+
resolveCustomFieldDisplay,
|
|
23
28
|
} from '@cavuno/board/format';
|
|
24
29
|
|
|
25
30
|
const { language } = await board.context();
|
|
26
31
|
|
|
27
|
-
formatSalaryRange(
|
|
32
|
+
const salary = formatSalaryRange(
|
|
28
33
|
language,
|
|
29
34
|
job.salaryMin,
|
|
30
35
|
job.salaryMax,
|
|
31
36
|
job.salaryTimeframe,
|
|
32
37
|
job.salaryCurrency);
|
|
38
|
+
// salary: { text, timeframe, bound: 'range' | 'from' | 'upTo' } | null
|
|
39
|
+
// Pieces — application composes order (do not paste a join template):
|
|
40
|
+
// text = Intl amount/range (e.g. "$90–120K", "90.000–120.000 €")
|
|
41
|
+
// timeframe = the WIRE ENUM ("per_year" … "per_hour") or null —
|
|
42
|
+
// never a word. Map it through your own catalog.
|
|
43
|
+
// bound = which open-range chrome word to attach, if any
|
|
44
|
+
// When joining text + timeframe (or any mixed-direction operands), isolate
|
|
45
|
+
// each side: U+2066 FSI … U+2069 PDI, or HTML <bdi> / dir="auto".
|
|
46
|
+
// Small rates default to standard by magnitude (|value| < 1000) so
|
|
47
|
+
// cents survive: formatSalaryRange(lang, 22.5, null, 'per_hour', 'USD')
|
|
48
|
+
// → { text: "$22.50", timeframe: "per_hour", bound: "from" }
|
|
49
|
+
// Pass notation 'compact' only when you want forced compact glue.
|
|
33
50
|
formatPublishedRelativeDate(language, job.publishedAt);
|
|
34
51
|
formatDate(language, job.publishedAt);
|
|
35
|
-
|
|
52
|
+
|
|
53
|
+
// Salary-page stats (currency required — not USD-hardcoded).
|
|
54
|
+
// notation matches formatSalaryRange: omit for magnitude default
|
|
55
|
+
// (|value| ≥ 1000 → compact; smaller → standard). Pass 'standard' for
|
|
56
|
+
// full figures ($90,000) or 'compact' to force short form.
|
|
57
|
+
formatSalaryStat(language, 90000, detail.currency);
|
|
58
|
+
formatSalaryStatRange(language, 90000, 120000, detail.currency);
|
|
59
|
+
formatSalaryStatRange(language, 90000, 120000, detail.currency, 'standard');
|
|
60
|
+
|
|
61
|
+
// Custom fields — language first. number → kind: 'number' (raw);
|
|
62
|
+
// multi_select → kind: 'multi_select' with values: string[] (labels).
|
|
63
|
+
// Do not String(n). Do not join multi-select in the SDK — app owns list
|
|
64
|
+
// style (conjunction / short / chips):
|
|
65
|
+
// new Intl.ListFormat(language, { style: 'long', type: 'conjunction' })
|
|
66
|
+
// .format(entry.values)
|
|
67
|
+
const fields = resolveCustomFieldDisplay(
|
|
68
|
+
language,
|
|
69
|
+
context.customFields.job,
|
|
70
|
+
job.customFieldValues);
|
|
36
71
|
```
|
|
37
72
|
|
|
38
|
-
`formatPublishedRelativeDate` produces the
|
|
39
|
-
cards and the job-detail header
|
|
40
|
-
UTC-pinned
|
|
41
|
-
|
|
42
|
-
job-publication label.
|
|
73
|
+
`formatPublishedRelativeDate` produces the short relative value used on job
|
|
74
|
+
cards and the job-detail header (locale-owned RTF `style: 'short'`). `formatDate`
|
|
75
|
+
and `formatMonthYear` produce UTC-pinned absolute forms for blog metadata
|
|
76
|
+
and detail facts.
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
The salary timeframe ships as the wire enum on `timeframe` alone —
|
|
79
|
+
not glued to `text` with a space or `/`. Open-range chrome is also
|
|
80
|
+
application-owned via `bound`. Compose amount, unit, and chrome in
|
|
81
|
+
board-language order (prefix, postfix, particles, no-space gluing for ja/zh),
|
|
82
|
+
with bidi isolation when directions may differ.
|
|
48
83
|
|
|
49
|
-
|
|
84
|
+
Missing/`null` currency is not treated as USD: helpers return `null`. Pass the
|
|
85
|
+
job's real `salaryCurrency`. Invalid or unsupported locales return `null`
|
|
86
|
+
rather than English or host-default fallbacks; underscore tags like `ja_JP`
|
|
87
|
+
are normalized to BCP-47 and checked with `Intl.NumberFormat.supportedLocalesOf`.
|
|
50
88
|
|
|
51
|
-
|
|
52
|
-
detail requires the full-job formatter. Convert a full saved job before
|
|
53
|
-
placing it in a card collection.
|
|
89
|
+
## Saved-job cards
|
|
54
90
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cardLocationLabel,
|
|
58
|
-
fullJobToCard,
|
|
59
|
-
locationLabel,
|
|
60
|
-
} from '@cavuno/board/format';
|
|
61
|
-
|
|
62
|
-
cardLocationLabel(language, card);
|
|
63
|
-
locationLabel(language, job);
|
|
64
|
-
const savedCard = fullJobToCard(language, job);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Build salary metadata copy
|
|
68
|
-
|
|
69
|
-
`getSalaryLexicon` exposes hosted salary terminology and sentence frames.
|
|
91
|
+
`me/saved-jobs` embeds the same slim `job_card` as the jobs list. Map it with
|
|
92
|
+
the same card view-model as listings — do not convert a full job client-side.
|
|
70
93
|
|
|
71
94
|
```ts snippet
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const lexicon = getSalaryLexicon(language);
|
|
75
|
-
lexicon.frames.entitySalariesTitle({
|
|
76
|
-
entity: 'JavaScript',
|
|
77
|
-
range: '$70K – $90K',
|
|
78
|
-
});
|
|
79
|
-
lexicon.seniority.senior;
|
|
95
|
+
// saved.job is already PublicJobCard
|
|
96
|
+
const card = toJobCardVM(saved.job, { language, /* … */ });
|
|
80
97
|
```
|
|
81
98
|
|
|
82
|
-
Use the helpers as the formatting boundary: pass the board language, wire
|
|
83
|
-
amounts, wire currency, and server location data in; render the returned text.
|
|
84
|
-
This keeps symbol placement, timeframe copy, number rules, and hosted labels
|
|
85
|
-
aligned. Sort-dropdown copy remains in `cavuno-board-filters`.
|
|
86
|
-
|
|
87
99
|
## Completion gate
|
|
88
100
|
|
|
89
101
|
Finish only after every applicable check passes:
|
|
90
102
|
|
|
91
103
|
- Every label-producing call receives `board.context().language`; no call site
|
|
92
|
-
substitutes a hardcoded locale.
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
- Formatting changes neither salary amounts nor
|
|
104
|
+
substitutes a hardcoded locale or relies on a silent `en` default.
|
|
105
|
+
- Amount and timeframe stay separate until the application joins them; the SDK
|
|
106
|
+
does not emit a pre-joined `"$90K / year"`.
|
|
107
|
+
- Open ranges return `bound: 'from' | 'upTo'`; the application adds open-range
|
|
108
|
+
chrome and composes order itself.
|
|
109
|
+
- Saved jobs arrive as cards; no `fullJobToCard` conversion.
|
|
110
|
+
- Salary-stat formatters receive the detail's `currency` (never invent USD).
|
|
111
|
+
- Formatting changes neither salary amounts nor invents chrome words.
|
|
100
112
|
|
|
101
113
|
## Cavuno SDK reference
|
|
102
114
|
|
|
@@ -1,55 +1,136 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-i18n
|
|
3
|
-
description: One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing,
|
|
3
|
+
description: One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing, or preserving board-language content.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# One-seam board localization
|
|
7
7
|
|
|
8
8
|
A Cavuno board has one content language: `board.context().language`. Jobs, companies, and blog content remain in that API-served language. Multi-locale frontends localize chrome—labels, headings, filters, and FAQ scaffolding—plus entity data the API already translates, such as taxonomy, places, and salary names.
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
All UI strings resolve through one application module. Layer copy in this order:
|
|
10
|
+
## Own the chrome catalog
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
Applications own chrome words. The SDK does not export a chrome-copy runtime
|
|
13
|
+
or seed `messages/`. Keep a typed copy seam in application code
|
|
14
|
+
(the starter already commits `messages/*.json` for en/de/fr).
|
|
17
15
|
|
|
18
16
|
```ts snippet
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
labels?: BoardLabelOverrides) {
|
|
27
|
-
return uiCopy(language, labels);
|
|
28
|
-
}
|
|
17
|
+
// Application-owned: load messages/ for the active locale and expose a
|
|
18
|
+
// seam components import. Shape is a fixed set of string groups (jobCard,
|
|
19
|
+
// jobSearch, jobDetail, apply, alerts, …).
|
|
20
|
+
export type BoardCopy = {
|
|
21
|
+
jobCard: { featuredLabel: string; /* … */ };
|
|
22
|
+
// …
|
|
23
|
+
};
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
## Build the copy seam
|
|
27
|
+
|
|
28
|
+
All UI strings resolve through one application module. Prefer compile-time messages (Paraglide / inlang) fed by application-owned `messages/` files:
|
|
29
|
+
|
|
30
|
+
1. Keep `messages/` in the application (or starter).
|
|
31
|
+
2. Compile messages with your framework's i18n tool.
|
|
32
|
+
3. Expose one seam modules components import (`src/copy.ts` or equivalent).
|
|
33
|
+
|
|
34
|
+
Components import this seam rather than reading JSON ad hoc. That keeps the catalog-to-generated-code transition to one module.
|
|
32
35
|
|
|
33
36
|
## Add locales
|
|
34
37
|
|
|
35
38
|
Use the installed framework and i18n guidance for routing, SSR locale detection, link rewriting, and compiled messages. Keep the board language unprefixed at `/` and prefix additional locales such as `/de/` or `/fr/`. Preserve these Cavuno invariants while doing so:
|
|
36
39
|
|
|
37
|
-
1.
|
|
38
|
-
2. Point the seam at the URL locale
|
|
40
|
+
1. Start from your application message catalogs so chrome wording is under version control with the app.
|
|
41
|
+
2. Point the seam at the URL locale. This step is complete when switching locale changes chrome.
|
|
39
42
|
3. Keep API content in `board.context().language`. This step is complete when locale switching leaves job, company, and blog fields unchanged.
|
|
40
43
|
4. Emit locale-aware `<html lang>`, canonical URLs, `hreflang`, and sitemap entries. This step is complete when each public locale route declares itself and all alternates consistently.
|
|
41
44
|
|
|
42
45
|
A compile-time i18n system can replace the seam's backing source. Keep the seam provider-free unless the chosen framework integration itself requires a provider.
|
|
43
46
|
|
|
47
|
+
## Own the five obligations
|
|
48
|
+
|
|
49
|
+
The SDK formats amounts, units, and relative dates with `Intl` and returns
|
|
50
|
+
structure. It does **not** ship plural selection, gender agreement, bidi
|
|
51
|
+
isolation, grapheme-safe truncation, or locale-aware case mapping. Those
|
|
52
|
+
five are application-owned. Satisfying the chrome / routing
|
|
53
|
+
gates alone is not enough — a board that ships without plural rules or
|
|
54
|
+
bidi isolation still fails this skill.
|
|
55
|
+
|
|
56
|
+
### Plural selection
|
|
57
|
+
|
|
58
|
+
Never branch on `count === 1`. Use `Intl.PluralRules` for the active locale
|
|
59
|
+
and pick the catalog form for the returned category:
|
|
60
|
+
|
|
61
|
+
```ts snippet
|
|
62
|
+
const pr = new Intl.PluralRules(locale);
|
|
63
|
+
// categories: zero | one | two | few | many | other (locale-dependent)
|
|
64
|
+
const form = messages.jobCount[pr.select(count)];
|
|
65
|
+
// en: { one: "{n} job", other: "{n} jobs" }
|
|
66
|
+
// ar: { zero, one, two, few, many, other } — all six can be required
|
|
67
|
+
const label = form.replace('{n}', new Intl.NumberFormat(locale).format(count));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use this for every count-bearing chrome string (search results, FAQ sample
|
|
71
|
+
sizes, alert copy). Do not hardcode English `-s`.
|
|
72
|
+
|
|
73
|
+
### Gender agreement
|
|
74
|
+
|
|
75
|
+
When a catalog string agrees with a person or role (adjectives, participles
|
|
76
|
+
in fr/de/ru/…), store gendered variants in the catalog and select by the
|
|
77
|
+
entity's gender metadata — do not invent agreement in code from English
|
|
78
|
+
stems.
|
|
79
|
+
|
|
80
|
+
### Bidi isolation
|
|
81
|
+
|
|
82
|
+
When composing SDK display strings with chrome or with mixed-direction
|
|
83
|
+
data (Latin company name inside an RTL sentence is the standard case),
|
|
84
|
+
isolate each operand whose direction may differ:
|
|
85
|
+
|
|
86
|
+
```ts snippet
|
|
87
|
+
const FSI = '\u2066'; // FIRST STRONG ISOLATE
|
|
88
|
+
const PDI = '\u2069'; // POP DIRECTIONAL ISOLATE
|
|
89
|
+
// Prefer HTML <bdi> / dir="auto" in markup; FSI/PDI in plain-text joins.
|
|
90
|
+
// `salary.timeframe` is the wire enum (`per_year`) — resolve it through
|
|
91
|
+
// your catalog first, then isolate both operands before joining.
|
|
92
|
+
const unit = salary.timeframe ? m[`salary_${salary.timeframe}`]() : '';
|
|
93
|
+
const line = `${FSI}${salary.text}${PDI} ${FSI}${unit}${PDI}`;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Never paste a bare `` `${amount} ${label}` `` into RTL chrome without
|
|
97
|
+
isolation. The SDK returns bare `string` values on purpose — isolation is
|
|
98
|
+
app-owned at the join site.
|
|
99
|
+
|
|
100
|
+
### Grapheme-safe truncation
|
|
101
|
+
|
|
102
|
+
Do not slice with `string.length` / `substring` on user-visible text.
|
|
103
|
+
Count grapheme clusters (`Intl.Segmenter` with `granularity: 'grapheme'`,
|
|
104
|
+
or a well-tested grapheme library) so emoji ZWJ sequences and combining
|
|
105
|
+
marks are not split.
|
|
106
|
+
|
|
107
|
+
### Locale-aware case mapping
|
|
108
|
+
|
|
109
|
+
Use `toLocaleLowerCase(locale)` / `toLocaleUpperCase(locale)` for display
|
|
110
|
+
case (Turkish `i`/`İ`, Greek final sigma). Keep identifier folding
|
|
111
|
+
(`toLowerCase` without locale) only for URL slugs and Set keys — the SDK
|
|
112
|
+
does that on purpose.
|
|
113
|
+
|
|
114
|
+
### Native digits
|
|
115
|
+
|
|
116
|
+
When formatting numbers the app owns (custom-field `kind: 'number'`,
|
|
117
|
+
counts, pagination), use `Intl.NumberFormat(locale)` so native-digit
|
|
118
|
+
locales (`ar`, `bn`, `hi-u-nu-deva`, …) render correctly. Never
|
|
119
|
+
`String(n)` for display.
|
|
120
|
+
|
|
44
121
|
## Completion gate
|
|
45
122
|
|
|
46
123
|
- Every authored chrome string enters components through the copy seam.
|
|
47
|
-
-
|
|
124
|
+
- Chrome comes from application messages, not from `board.context()` (the API no longer serves a labels bag).
|
|
48
125
|
- Each supported locale renders translated chrome at its public URL.
|
|
49
126
|
- Switching locale preserves API-served job, company, and blog content.
|
|
50
127
|
- Server HTML uses the active locale in `<html lang>` with no hydration mismatch.
|
|
51
128
|
- Canonical, `hreflang`, and sitemap URLs agree for every locale route.
|
|
52
129
|
- Missing translation keys fail generation or tests rather than appearing as raw keys.
|
|
130
|
+
- Count-bearing chrome uses `Intl.PluralRules` (no `count === 1` English branches).
|
|
131
|
+
- Composed display strings that mix directions isolate operands (FSI/PDI or `<bdi>`).
|
|
132
|
+
- Visible truncation is grapheme-safe; display case mapping uses the locale.
|
|
133
|
+
- App-owned numbers format via `Intl.NumberFormat`, not `String(n)`.
|
|
53
134
|
|
|
54
135
|
## Cavuno SDK reference
|
|
55
136
|
|
|
@@ -5,9 +5,9 @@ description: Build server-ranked search suggestions with @cavuno/board. Use for
|
|
|
5
5
|
|
|
6
6
|
# Server-ranked search suggestions
|
|
7
7
|
|
|
8
|
-
`board.search.suggest` returns one interleaved list of companies
|
|
8
|
+
`board.search.suggest` returns one interleaved list of companies, markets, taxonomy terms, blog posts, and tags. Pass `types` to restrict kinds; `limit` applies after that filter. Server rank is the contract: render its order unchanged. For UI, the headless controller adds debounce, abort, and stale-result dropping.
|
|
9
9
|
|
|
10
|
-
Location autocomplete uses `board.taxonomy.places.list({ q })`; filter option lists use `board.taxonomy.categories.list
|
|
10
|
+
Location autocomplete uses `board.taxonomy.places.list({ q })`; filter option lists use `board.taxonomy.categories.list` or `board.taxonomy.skills.list`; job results use `board.jobs.list` or `board.jobs.search`. Route a selected suggestion with `suggestionPath` from `@cavuno/board/paths`.
|
|
11
11
|
|
|
12
12
|
## Read suggestions
|
|
13
13
|
|
|
@@ -15,6 +15,7 @@ Location autocomplete uses `board.taxonomy.places.list({ q })`; filter option li
|
|
|
15
15
|
const { items, query } = await board.search.suggest({
|
|
16
16
|
q: 'acme',
|
|
17
17
|
limit: 10,
|
|
18
|
+
types: ['company', 'skill', 'category'],
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
for (const item of items) {
|
|
@@ -22,6 +23,10 @@ for (const item of items) {
|
|
|
22
23
|
item.slug;
|
|
23
24
|
item.name;
|
|
24
25
|
item.jobCount;
|
|
26
|
+
} else if (item.type === 'market') {
|
|
27
|
+
item.slug;
|
|
28
|
+
item.name;
|
|
29
|
+
item.companyCount;
|
|
25
30
|
} else {
|
|
26
31
|
item.termType;
|
|
27
32
|
item.displayName;
|
|
@@ -31,7 +36,7 @@ for (const item of items) {
|
|
|
31
36
|
}
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
Queries shorter than two characters return no items. `limit` accepts 1–25 and defaults to 25.
|
|
39
|
+
Queries shorter than two characters return no items. `limit` accepts 1–25 and defaults to 25. `{ types: ['skill'], limit: 10 }` returns up to ten skills.
|
|
35
40
|
|
|
36
41
|
Term suggestions have `termType: 'category' | 'skill'`; use `canonicalSlug` for links and `sourceSlug` for job filters.
|
|
37
42
|
|
|
@@ -21,10 +21,13 @@ const author = await board.blog.authors.retrieve('jane');
|
|
|
21
21
|
const { data: authorPosts } = await board.blog.posts.list({
|
|
22
22
|
authorSlug: 'jane',
|
|
23
23
|
});
|
|
24
|
+
// Profile description is application-owned. Prefer author.bio; otherwise
|
|
25
|
+
// compose from the board catalog (shape only — not an English sentence).
|
|
24
26
|
const profile = createAuthorProfileJsonLd({
|
|
25
27
|
author,
|
|
26
28
|
canonical: `https://jobs.example.com/blog/author/${author.slug}`,
|
|
27
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
author.bio ?? messages.authorProfileDescription({ name: author.name }),
|
|
28
31
|
origin: 'https://jobs.example.com',
|
|
29
32
|
posts: authorPosts,
|
|
30
33
|
totalPosts: authorPosts.length,
|
|
@@ -32,13 +32,16 @@ Labels are trimmed, blank crumbs are dropped, and fewer than two surviving
|
|
|
32
32
|
crumbs return `null`. The current page omits `href`, which makes the builder
|
|
33
33
|
omit its `item`. Supply absolute hrefs, or pass `origin` for path hrefs.
|
|
34
34
|
|
|
35
|
+
Breadcrumb labels are application-owned — pull them from the board's message
|
|
36
|
+
catalog. The snippet shows structure only, not English copy.
|
|
37
|
+
|
|
35
38
|
```ts snippet
|
|
36
39
|
import { createBreadcrumbJsonLd } from '@cavuno/board/seo';
|
|
37
40
|
|
|
38
41
|
const trail = createBreadcrumbJsonLd(
|
|
39
42
|
[
|
|
40
|
-
{ label: '
|
|
41
|
-
{ label: '
|
|
43
|
+
{ label: messages.breadcrumb('home'), href: '/' },
|
|
44
|
+
{ label: messages.breadcrumb('jobs'), href: '/jobs' },
|
|
42
45
|
{ label: job.title },
|
|
43
46
|
],
|
|
44
47
|
{ origin: 'https://jobs.example.com' });
|
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
# Listing metadata and JSON-LD
|
|
2
2
|
|
|
3
3
|
Read this reference for a job-listing page's head descriptors and structural
|
|
4
|
-
JSON-LD. These builders
|
|
5
|
-
|
|
4
|
+
JSON-LD. These builders assemble structure only; supply the page's resolved
|
|
5
|
+
title and meta description from the board's copy source.
|
|
6
6
|
|
|
7
7
|
```ts snippet
|
|
8
8
|
import { listingHead, listingJsonLd } from '@cavuno/board/seo';
|
|
9
9
|
|
|
10
|
-
const { name } = await board.context();
|
|
11
|
-
const
|
|
10
|
+
const { name, language } = await board.context();
|
|
11
|
+
const count = 42;
|
|
12
|
+
const heading = /* resolved page heading from your catalog */ '…';
|
|
13
|
+
// Application owns every display join — count, counter/particle, heading,
|
|
14
|
+
// separator, board name. Pieces (not a pasteable template):
|
|
15
|
+
// countLabel = Intl.NumberFormat(language).format(count)
|
|
16
|
+
// heading = catalog heading for this page
|
|
17
|
+
// boardName = name
|
|
18
|
+
// Compose title and description in the board language (ja counters, RTL order,
|
|
19
|
+
// …). Do not hardcode number-then-noun or " | " order in shared helpers.
|
|
20
|
+
const countLabel = new Intl.NumberFormat(language).format(count);
|
|
21
|
+
const title = messages.listingTitle({ count, countLabel, heading, boardName: name });
|
|
22
|
+
const description = messages.listingDescription({
|
|
23
|
+
count,
|
|
24
|
+
countLabel,
|
|
25
|
+
heading,
|
|
12
26
|
boardName: name,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const head = listingHead({
|
|
30
|
+
title,
|
|
13
31
|
origin: 'https://jobs.example.com',
|
|
14
32
|
path: '/jobs/robotics',
|
|
15
|
-
|
|
16
|
-
count: 42,
|
|
33
|
+
description,
|
|
17
34
|
});
|
|
18
35
|
|
|
36
|
+
// Breadcrumb labels are application-owned (catalog), not English literals.
|
|
19
37
|
const objects = listingJsonLd({
|
|
20
38
|
origin: 'https://jobs.example.com',
|
|
21
|
-
breadcrumbs: [
|
|
39
|
+
breadcrumbs: [
|
|
40
|
+
{ name: messages.breadcrumb('home'), path: '/' },
|
|
41
|
+
{ name: messages.breadcrumb('jobs') },
|
|
42
|
+
],
|
|
22
43
|
jobs: cards.map((card) => ({
|
|
23
44
|
slug: card.slug,
|
|
24
45
|
company: card.company,
|
|
@@ -27,10 +48,12 @@ const objects = listingJsonLd({
|
|
|
27
48
|
```
|
|
28
49
|
|
|
29
50
|
`head.meta` contains title, description, and Open Graph descriptors;
|
|
30
|
-
`head.links` contains the canonical link.
|
|
31
|
-
|
|
51
|
+
`head.links` contains the canonical link. Title and description are whatever
|
|
52
|
+
you passed — the SDK does not join count, heading, or board name. Use a
|
|
53
|
+
locale-aware lowercasing API (not `.toLowerCase()`) when your copy needs case
|
|
54
|
+
folding.
|
|
32
55
|
|
|
33
56
|
This branch is complete when head descriptors reflect the resolved page
|
|
34
|
-
|
|
35
|
-
and `listingJsonLd` receives card slugs plus company objects from the
|
|
36
|
-
response.
|
|
57
|
+
title and description, the canonical URL combines the intended origin and
|
|
58
|
+
path, and `listingJsonLd` receives card slugs plus company objects from the
|
|
59
|
+
listing response.
|
|
@@ -6,15 +6,17 @@ seniority ordering.
|
|
|
6
6
|
## Build a title page
|
|
7
7
|
|
|
8
8
|
Display-producing helpers require the board language as their leading
|
|
9
|
-
argument.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
argument. Salary money formatters take the detail's `currency` as well —
|
|
10
|
+
locale drives digits/grouping; currency is never USD-hardcoded. FAQ helpers
|
|
11
|
+
return entry kinds and values; compose question/answer prose from the board's
|
|
12
|
+
message catalog (with proper plural rules via `Intl.PluralRules` — never a
|
|
13
|
+
two-way `one`/else branch).
|
|
12
14
|
|
|
13
15
|
```ts snippet
|
|
14
16
|
import {
|
|
15
17
|
buildSalaryFaq,
|
|
16
18
|
faqJsonLd,
|
|
17
|
-
|
|
19
|
+
formatSalaryStatRange,
|
|
18
20
|
itemListJsonLd,
|
|
19
21
|
sortBySeniority,
|
|
20
22
|
titleSalaryJsonLd,
|
|
@@ -23,14 +25,57 @@ import {
|
|
|
23
25
|
const { language } = await board.context();
|
|
24
26
|
const detail = await board.salaries.titles.retrieve('software-engineer');
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
// Application owns the finished distribution name (label + word order).
|
|
29
|
+
// French: `${entity} ${catalog.seniority[seniority]}`; English often
|
|
30
|
+
// `${catalog.seniority[seniority]} ${entity}`; Japanese may omit the space.
|
|
31
|
+
const occupation = titleSalaryJsonLd(detail, {
|
|
32
|
+
seniorityName: ({ seniority, entity }) =>
|
|
33
|
+
copy.seniorityName({ seniority, entity }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Structure only — map kinds to localized FAQ sentences in the app.
|
|
37
|
+
// The application owns every word; the snippet shows shape, not copy.
|
|
38
|
+
// average carries avgMin/avgMax/currency (raw) + range (compact convenience).
|
|
39
|
+
// Prefer the raw figures when FAQ prose wants full notation ($100,000).
|
|
40
|
+
const entries = buildSalaryFaq(
|
|
41
|
+
language,
|
|
42
|
+
detail.categoryName,
|
|
43
|
+
detail.overallSalary,
|
|
44
|
+
detail.currency);
|
|
27
45
|
const faq = faqJsonLd(
|
|
28
|
-
|
|
46
|
+
entries.map((entry) => {
|
|
47
|
+
if (entry.kind === 'average') {
|
|
48
|
+
const range =
|
|
49
|
+
formatSalaryStatRange(
|
|
50
|
+
language,
|
|
51
|
+
entry.avgMin,
|
|
52
|
+
entry.avgMax,
|
|
53
|
+
entry.currency,
|
|
54
|
+
'standard') ?? entry.range;
|
|
55
|
+
return {
|
|
56
|
+
// Application catalog — use Intl.PluralRules for jobCount, never a
|
|
57
|
+
// one/else English plural.
|
|
58
|
+
q: copy.faqQuestion({ kind: 'average', label: entry.label }),
|
|
59
|
+
a: copy.faqAnswer({
|
|
60
|
+
kind: 'average',
|
|
61
|
+
label: entry.label,
|
|
62
|
+
range,
|
|
63
|
+
jobCount: entry.jobCount,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
q: copy.faqQuestion({ kind: 'methodology', label: entry.label }),
|
|
69
|
+
a: copy.faqAnswer({ kind: 'methodology', label: entry.label }),
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
|
|
29
73
|
const range = detail.overallSalary
|
|
30
|
-
?
|
|
74
|
+
? formatSalaryStatRange(
|
|
31
75
|
language,
|
|
32
76
|
detail.overallSalary.avgMin,
|
|
33
|
-
detail.overallSalary.avgMax
|
|
77
|
+
detail.overallSalary.avgMax,
|
|
78
|
+
detail.currency)
|
|
34
79
|
: null;
|
|
35
80
|
const ladder = sortBySeniority(detail.bySeniority);
|
|
36
81
|
const index = itemListJsonLd([
|
|
@@ -44,18 +89,38 @@ const index = itemListJsonLd([
|
|
|
44
89
|
## Select other salary builders
|
|
45
90
|
|
|
46
91
|
- `skillSalaryJsonLd(detail)` for a skill detail.
|
|
47
|
-
- `locationSalaryJsonLd(detail)` for a city-level location
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- `
|
|
51
|
-
|
|
52
|
-
- `
|
|
92
|
+
- `locationSalaryJsonLd(detail, { occupationUrl })` for a city-level location
|
|
93
|
+
detail — emits an `ItemList` of top categories (not an `Occupation` named
|
|
94
|
+
after the city). Supply absolute URLs to each title salary page.
|
|
95
|
+
- `crossAxisSalaryJsonLd(args, { seniorityName? })` for a title/skill and
|
|
96
|
+
location pair.
|
|
97
|
+
- `companySalaryJsonLd(detail, { occupationUrl })` for a company overview —
|
|
98
|
+
`ItemList` of categories at that employer (not an occupation named after
|
|
99
|
+
the company).
|
|
100
|
+
- `companyCategorySalaryJsonLd(detail, { seniorityName? })` for a
|
|
101
|
+
company-category page (keeps `Occupation`).
|
|
102
|
+
- `formatSalaryStat(locale, value, currency, notation?)` /
|
|
103
|
+
`formatSalaryStatRange(locale, min, max, currency, notation?)` for
|
|
104
|
+
salary-page amounts (currency from the detail, not hardcoded USD;
|
|
105
|
+
`formatRange` owns the join). `notation` matches `formatSalaryRange`
|
|
106
|
+
(magnitude default when omitted; pass `'standard'` for full figures).
|
|
107
|
+
Returns `null` when currency is empty or `Intl` rejects inputs.
|
|
108
|
+
- `SENIORITY_ORDER` / `sortBySeniority` for seniority ladder ordering
|
|
109
|
+
(display names are application-owned via `seniorityName`).
|
|
110
|
+
|
|
111
|
+
JSON-LD `name` fields are data labels (entity and category names).
|
|
112
|
+
Per-seniority distribution names come only from `seniorityName`; when that
|
|
113
|
+
callback is omitted, those distributions ship without a `name`. Do not expect
|
|
114
|
+
English framing such as "salary (all levels)" or "Average Salary in …" from
|
|
115
|
+
the SDK — put those words in the app if a board still wants them.
|
|
53
116
|
|
|
54
|
-
The structured-data families are `Occupation
|
|
55
|
-
|
|
56
|
-
|
|
117
|
+
The structured-data families are `Occupation` (title, skill, cross-axis,
|
|
118
|
+
company-category), `ItemList` (location overview, company overview, indexes),
|
|
119
|
+
`MonetaryAmountDistribution`, and `FAQPage`. Feed them the SDK salary read
|
|
120
|
+
model; preserve nullable aggregates and server-computed values.
|
|
57
121
|
|
|
58
122
|
This branch is complete when every applicable schema object has the expected
|
|
59
123
|
schema.org type, null aggregates produce no fabricated figure, the board
|
|
60
|
-
language comes from context,
|
|
124
|
+
language comes from context, FAQ prose comes from the application catalog
|
|
125
|
+
with proper plural rules, and every displayed value traces to a response
|
|
61
126
|
field.
|
|
@@ -20,8 +20,9 @@ Read the matching reference completely before implementing that page:
|
|
|
20
20
|
For a page that combines branches, read every matching reference.
|
|
21
21
|
|
|
22
22
|
Sitemaps and robots.txt use `cavuno-board-sitemap`. The host app owns OG-image
|
|
23
|
-
generation and its route map. `listingHead` receives app-owned
|
|
24
|
-
the SDK
|
|
23
|
+
generation and its route map. `listingHead` receives app-owned title and
|
|
24
|
+
meta description copy; the SDK never composes those sentences or joins
|
|
25
|
+
count/heading/board name.
|
|
25
26
|
|
|
26
27
|
## Apply the shared rendering contract
|
|
27
28
|
|
package/skills/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "4.1.0",
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "cavuno-board-account",
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "cavuno-board-format",
|
|
62
|
-
"description": "Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates,
|
|
62
|
+
"description": "Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates, custom-field display, or salary-stat numbers.",
|
|
63
63
|
"path": "skills/cavuno-board-format/SKILL.md",
|
|
64
64
|
"framework": null,
|
|
65
65
|
"category": "core"
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
"name": "cavuno-board-i18n",
|
|
69
|
-
"description": "One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing,
|
|
69
|
+
"description": "One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing, or preserving board-language content.",
|
|
70
70
|
"path": "skills/cavuno-board-i18n/SKILL.md",
|
|
71
71
|
"framework": null,
|
|
72
72
|
"category": "core"
|