@cavuno/board 1.25.0 → 1.27.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.
Files changed (49) hide show
  1. package/README.md +88 -0
  2. package/dist/bin.mjs +67 -34
  3. package/dist/filters.d.mts +55 -0
  4. package/dist/filters.d.ts +55 -0
  5. package/dist/filters.js +193 -0
  6. package/dist/filters.mjs +170 -0
  7. package/dist/format.d.mts +240 -0
  8. package/dist/format.d.ts +240 -0
  9. package/dist/format.js +453 -0
  10. package/dist/format.mjs +430 -0
  11. package/dist/index.d.mts +81 -3965
  12. package/dist/index.d.ts +81 -3965
  13. package/dist/index.js +154 -7
  14. package/dist/index.mjs +151 -4
  15. package/dist/jobs-DK5mPBgq.d.mts +3836 -0
  16. package/dist/jobs-DK5mPBgq.d.ts +3836 -0
  17. package/dist/salaries-CXt6Vkrp.d.ts +130 -0
  18. package/dist/salaries-CrJsaZe6.d.mts +130 -0
  19. package/dist/seo.d.mts +288 -0
  20. package/dist/seo.d.ts +288 -0
  21. package/dist/seo.js +1102 -0
  22. package/dist/seo.mjs +1079 -0
  23. package/dist/sitemap.d.mts +63 -0
  24. package/dist/sitemap.d.ts +63 -0
  25. package/dist/sitemap.js +353 -0
  26. package/dist/sitemap.mjs +330 -0
  27. package/dist/theme.d.mts +63 -0
  28. package/dist/theme.d.ts +63 -0
  29. package/dist/theme.js +149 -0
  30. package/dist/theme.mjs +128 -0
  31. package/package.json +57 -2
  32. package/skills/cavuno-board-account/SKILL.md +147 -0
  33. package/skills/cavuno-board-applications/SKILL.md +110 -0
  34. package/skills/cavuno-board-blog/SKILL.md +99 -0
  35. package/skills/cavuno-board-companies/SKILL.md +99 -0
  36. package/skills/cavuno-board-filters/SKILL.md +89 -0
  37. package/skills/cavuno-board-format/SKILL.md +104 -0
  38. package/skills/cavuno-board-job-alerts/SKILL.md +115 -0
  39. package/skills/cavuno-board-job-posting/SKILL.md +130 -0
  40. package/skills/cavuno-board-jobs/SKILL.md +15 -0
  41. package/skills/cavuno-board-messaging/SKILL.md +121 -0
  42. package/skills/cavuno-board-paywall/SKILL.md +108 -0
  43. package/skills/cavuno-board-salaries/SKILL.md +87 -0
  44. package/skills/cavuno-board-seo/SKILL.md +180 -0
  45. package/skills/cavuno-board-setup/SKILL.md +26 -2
  46. package/skills/cavuno-board-sitemap/SKILL.md +147 -0
  47. package/skills/cavuno-board-smoke-test/SKILL.md +84 -0
  48. package/skills/cavuno-board-theme/SKILL.md +69 -0
  49. package/skills/manifest.json +106 -1
@@ -0,0 +1,63 @@
1
+ import { BoardSdk } from './index.mjs';
2
+ import './jobs-DK5mPBgq.mjs';
3
+ import './salaries-CrJsaZe6.mjs';
4
+
5
+ /**
6
+ * Sitemap primitives — the pure XML + bucket-filename logic behind a board
7
+ * frontend's `/sitemap.xml` index and `/sitemap/:file` bucket routes.
8
+ * Mirrors the hosted board's 8-bucket model (`board-sitemap.loader.ts`): a
9
+ * sitemap index points at one file per content bucket, each an ordinary
10
+ * `<urlset>`.
11
+ *
12
+ * The XML byte layout transcribes the hosted serializers
13
+ * (`sitemap-xml.utils.ts` — `serializeBoardSitemap` /
14
+ * `serializeBoardSitemapIndex`) and is golden-tested byte-equal against
15
+ * them in-monorepo. `changefreq`/`priority` are deliberately not supported:
16
+ * the hosted builders never emit them.
17
+ *
18
+ * Network enumeration lives in `walker.ts`; this module is pure so the
19
+ * chunking + filename round-trip is unit-tested without a board.
20
+ */
21
+ declare const SITEMAP_BUCKETS: readonly ["marketing", "jobs-categories", "jobs-skills", "jobs-locations", "jobs-details", "companies", "salaries", "blog"];
22
+ type SitemapBucket = (typeof SITEMAP_BUCKETS)[number];
23
+ /** Matches the hosted chunk size so the two corpora line up bucket-for-bucket. */
24
+ declare const SITEMAP_CHUNK_SIZE = 45000;
25
+ declare function xmlEscape(value: string): string;
26
+ /**
27
+ * Chunk 0 (or no chunk) → the bare bucket filename; chunks 1+ append `-2`,
28
+ * `-3`, … so the common single-chunk case has the cleanest URL. The scheme
29
+ * is the filename component of the hosted `getBoardSitemapBucketPath`.
30
+ */
31
+ declare function bucketFilename(bucket: SitemapBucket, chunkIndex?: number): string;
32
+ /** Inverse of `bucketFilename`; returns null for unknown buckets / non-xml. */
33
+ declare function parseBucketFilename(filename: string): {
34
+ bucket: SitemapBucket;
35
+ chunkIndex: number;
36
+ } | null;
37
+ declare function chunk<T>(items: readonly T[], size: number): T[][];
38
+ /**
39
+ * One `<url>` entry. A bare string is shorthand for `{ url }` — the walker
40
+ * emits plain URL strings; hosted-shaped entries carry `lastModified` (and
41
+ * `images` on job-detail entries: the company logo).
42
+ */
43
+ interface SitemapUrlEntry {
44
+ url: string;
45
+ /** A `Date` serializes to ISO 8601; a string passes through as-is. */
46
+ lastModified?: Date | string;
47
+ /** Google image-sitemap extension URLs (adds the `xmlns:image` namespace). */
48
+ images?: readonly string[];
49
+ }
50
+ interface SitemapIndexEntry {
51
+ url: string;
52
+ lastModified?: Date | string;
53
+ }
54
+ declare function renderUrlset(entries: readonly (string | SitemapUrlEntry)[]): string;
55
+ declare function renderSitemapIndex(entries: readonly (string | SitemapIndexEntry)[]): string;
56
+
57
+ /** Matches the hosted thin-content floor: a listing page needs ≥5 jobs to index. */
58
+ declare const MIN_JOBS_PER_INDEXED_PAGE = 5;
59
+ /** Which buckets the index lists. Blog is gated by its feature; the rest emit an empty urlset when a board lacks that content (valid, just zero URLs). */
60
+ declare function listedBuckets(board: BoardSdk): Promise<SitemapBucket[]>;
61
+ declare function buildBucketUrls(board: BoardSdk, origin: string, bucket: SitemapBucket): Promise<string[]>;
62
+
63
+ export { MIN_JOBS_PER_INDEXED_PAGE, SITEMAP_BUCKETS, SITEMAP_CHUNK_SIZE, type SitemapBucket, type SitemapIndexEntry, type SitemapUrlEntry, bucketFilename, buildBucketUrls, chunk, listedBuckets, parseBucketFilename, renderSitemapIndex, renderUrlset, xmlEscape };
@@ -0,0 +1,63 @@
1
+ import { BoardSdk } from './index.js';
2
+ import './jobs-DK5mPBgq.js';
3
+ import './salaries-CXt6Vkrp.js';
4
+
5
+ /**
6
+ * Sitemap primitives — the pure XML + bucket-filename logic behind a board
7
+ * frontend's `/sitemap.xml` index and `/sitemap/:file` bucket routes.
8
+ * Mirrors the hosted board's 8-bucket model (`board-sitemap.loader.ts`): a
9
+ * sitemap index points at one file per content bucket, each an ordinary
10
+ * `<urlset>`.
11
+ *
12
+ * The XML byte layout transcribes the hosted serializers
13
+ * (`sitemap-xml.utils.ts` — `serializeBoardSitemap` /
14
+ * `serializeBoardSitemapIndex`) and is golden-tested byte-equal against
15
+ * them in-monorepo. `changefreq`/`priority` are deliberately not supported:
16
+ * the hosted builders never emit them.
17
+ *
18
+ * Network enumeration lives in `walker.ts`; this module is pure so the
19
+ * chunking + filename round-trip is unit-tested without a board.
20
+ */
21
+ declare const SITEMAP_BUCKETS: readonly ["marketing", "jobs-categories", "jobs-skills", "jobs-locations", "jobs-details", "companies", "salaries", "blog"];
22
+ type SitemapBucket = (typeof SITEMAP_BUCKETS)[number];
23
+ /** Matches the hosted chunk size so the two corpora line up bucket-for-bucket. */
24
+ declare const SITEMAP_CHUNK_SIZE = 45000;
25
+ declare function xmlEscape(value: string): string;
26
+ /**
27
+ * Chunk 0 (or no chunk) → the bare bucket filename; chunks 1+ append `-2`,
28
+ * `-3`, … so the common single-chunk case has the cleanest URL. The scheme
29
+ * is the filename component of the hosted `getBoardSitemapBucketPath`.
30
+ */
31
+ declare function bucketFilename(bucket: SitemapBucket, chunkIndex?: number): string;
32
+ /** Inverse of `bucketFilename`; returns null for unknown buckets / non-xml. */
33
+ declare function parseBucketFilename(filename: string): {
34
+ bucket: SitemapBucket;
35
+ chunkIndex: number;
36
+ } | null;
37
+ declare function chunk<T>(items: readonly T[], size: number): T[][];
38
+ /**
39
+ * One `<url>` entry. A bare string is shorthand for `{ url }` — the walker
40
+ * emits plain URL strings; hosted-shaped entries carry `lastModified` (and
41
+ * `images` on job-detail entries: the company logo).
42
+ */
43
+ interface SitemapUrlEntry {
44
+ url: string;
45
+ /** A `Date` serializes to ISO 8601; a string passes through as-is. */
46
+ lastModified?: Date | string;
47
+ /** Google image-sitemap extension URLs (adds the `xmlns:image` namespace). */
48
+ images?: readonly string[];
49
+ }
50
+ interface SitemapIndexEntry {
51
+ url: string;
52
+ lastModified?: Date | string;
53
+ }
54
+ declare function renderUrlset(entries: readonly (string | SitemapUrlEntry)[]): string;
55
+ declare function renderSitemapIndex(entries: readonly (string | SitemapIndexEntry)[]): string;
56
+
57
+ /** Matches the hosted thin-content floor: a listing page needs ≥5 jobs to index. */
58
+ declare const MIN_JOBS_PER_INDEXED_PAGE = 5;
59
+ /** Which buckets the index lists. Blog is gated by its feature; the rest emit an empty urlset when a board lacks that content (valid, just zero URLs). */
60
+ declare function listedBuckets(board: BoardSdk): Promise<SitemapBucket[]>;
61
+ declare function buildBucketUrls(board: BoardSdk, origin: string, bucket: SitemapBucket): Promise<string[]>;
62
+
63
+ export { MIN_JOBS_PER_INDEXED_PAGE, SITEMAP_BUCKETS, SITEMAP_CHUNK_SIZE, type SitemapBucket, type SitemapIndexEntry, type SitemapUrlEntry, bucketFilename, buildBucketUrls, chunk, listedBuckets, parseBucketFilename, renderSitemapIndex, renderUrlset, xmlEscape };
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/sitemap/index.ts
21
+ var sitemap_exports = {};
22
+ __export(sitemap_exports, {
23
+ MIN_JOBS_PER_INDEXED_PAGE: () => MIN_JOBS_PER_INDEXED_PAGE,
24
+ SITEMAP_BUCKETS: () => SITEMAP_BUCKETS,
25
+ SITEMAP_CHUNK_SIZE: () => SITEMAP_CHUNK_SIZE,
26
+ bucketFilename: () => bucketFilename,
27
+ buildBucketUrls: () => buildBucketUrls,
28
+ chunk: () => chunk,
29
+ listedBuckets: () => listedBuckets,
30
+ parseBucketFilename: () => parseBucketFilename,
31
+ renderSitemapIndex: () => renderSitemapIndex,
32
+ renderUrlset: () => renderUrlset,
33
+ xmlEscape: () => xmlEscape
34
+ });
35
+ module.exports = __toCommonJS(sitemap_exports);
36
+
37
+ // src/sitemap/xml.ts
38
+ var SITEMAP_BUCKETS = [
39
+ "marketing",
40
+ "jobs-categories",
41
+ "jobs-skills",
42
+ "jobs-locations",
43
+ "jobs-details",
44
+ "companies",
45
+ "salaries",
46
+ "blog"
47
+ ];
48
+ var SITEMAP_CHUNK_SIZE = 45e3;
49
+ function xmlEscape(value) {
50
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
51
+ }
52
+ function bucketFilename(bucket, chunkIndex = 0) {
53
+ const suffix = chunkIndex > 0 ? `-${chunkIndex + 1}` : "";
54
+ return `${bucket}${suffix}.xml`;
55
+ }
56
+ function parseBucketFilename(filename) {
57
+ if (!filename.endsWith(".xml")) return null;
58
+ const stem = filename.slice(0, -".xml".length);
59
+ const chunkMatch = stem.match(/^(.+?)-(\d+)$/);
60
+ if (chunkMatch) {
61
+ const candidate = chunkMatch[1];
62
+ const chunkNumber = Number.parseInt(chunkMatch[2] ?? "", 10);
63
+ if (SITEMAP_BUCKETS.includes(candidate) && Number.isFinite(chunkNumber) && chunkNumber >= 2) {
64
+ return { bucket: candidate, chunkIndex: chunkNumber - 1 };
65
+ }
66
+ }
67
+ if (SITEMAP_BUCKETS.includes(stem)) {
68
+ return { bucket: stem, chunkIndex: 0 };
69
+ }
70
+ return null;
71
+ }
72
+ function chunk(items, size) {
73
+ if (items.length === 0) return [];
74
+ if (items.length <= size) return [[...items]];
75
+ const out = [];
76
+ for (let i = 0; i < items.length; i += size) {
77
+ out.push(items.slice(i, i + size));
78
+ }
79
+ return out;
80
+ }
81
+ var XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>\n';
82
+ var SITEMAP_NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
83
+ var IMAGE_NS = "http://www.google.com/schemas/sitemap-image/1.1";
84
+ function lastmod(value) {
85
+ const serialized = value instanceof Date ? value.toISOString() : value;
86
+ return `<lastmod>${xmlEscape(serialized)}</lastmod>
87
+ `;
88
+ }
89
+ function renderUrlset(entries) {
90
+ const normalized = entries.map(
91
+ (entry) => typeof entry === "string" ? { url: entry } : entry
92
+ );
93
+ const hasImages = normalized.some((entry) => (entry.images?.length ?? 0) > 0);
94
+ const namespaces = [`xmlns="${SITEMAP_NS}"`];
95
+ if (hasImages) {
96
+ namespaces.push(`xmlns:image="${IMAGE_NS}"`);
97
+ }
98
+ let xml = XML_DECLARATION + `<urlset ${namespaces.join(" ")}>
99
+ `;
100
+ for (const entry of normalized) {
101
+ xml += "<url>\n";
102
+ xml += `<loc>${xmlEscape(entry.url)}</loc>
103
+ `;
104
+ if (entry.images?.length) {
105
+ for (const image of entry.images) {
106
+ xml += `<image:image>
107
+ <image:loc>${xmlEscape(image)}</image:loc>
108
+ </image:image>
109
+ `;
110
+ }
111
+ }
112
+ if (entry.lastModified) {
113
+ xml += lastmod(entry.lastModified);
114
+ }
115
+ xml += "</url>\n";
116
+ }
117
+ xml += "</urlset>\n";
118
+ return xml;
119
+ }
120
+ function renderSitemapIndex(entries) {
121
+ let xml = XML_DECLARATION + `<sitemapindex xmlns="${SITEMAP_NS}">
122
+ `;
123
+ for (const raw of entries) {
124
+ const entry = typeof raw === "string" ? { url: raw } : raw;
125
+ xml += "<sitemap>\n";
126
+ xml += `<loc>${xmlEscape(entry.url)}</loc>
127
+ `;
128
+ if (entry.lastModified) {
129
+ xml += lastmod(entry.lastModified);
130
+ }
131
+ xml += "</sitemap>\n";
132
+ }
133
+ xml += "</sitemapindex>\n";
134
+ return xml;
135
+ }
136
+
137
+ // src/pagination.ts
138
+ function paginate(listFn, query, options) {
139
+ async function* pages() {
140
+ let current = query;
141
+ for (; ; ) {
142
+ const page = await listFn(current, options);
143
+ yield page;
144
+ if (!page.hasMore || page.nextCursor === null) return;
145
+ const { offset: _offset, ...rest } = current ?? {};
146
+ current = { ...rest, cursor: page.nextCursor };
147
+ }
148
+ }
149
+ return {
150
+ pages,
151
+ async *[Symbol.asyncIterator]() {
152
+ for await (const page of pages()) {
153
+ yield* page.data;
154
+ }
155
+ },
156
+ async toArray({ limit }) {
157
+ const items = [];
158
+ for await (const page of pages()) {
159
+ items.push(...page.data);
160
+ if (items.length >= limit) break;
161
+ }
162
+ return items.slice(0, limit);
163
+ }
164
+ };
165
+ }
166
+
167
+ // src/sitemap/walker.ts
168
+ var MIN_JOBS_PER_INDEXED_PAGE = 5;
169
+ var MAX_PAGES = 200;
170
+ async function drainPages(pages) {
171
+ const acc = [];
172
+ let pageCount = 0;
173
+ for await (const page of pages) {
174
+ acc.push(...page.data);
175
+ pageCount += 1;
176
+ if (pageCount >= MAX_PAGES) {
177
+ if (page.hasMore) {
178
+ console.warn(
179
+ `[sitemap] hit the ${MAX_PAGES}-page pagination cap \u2014 output may be truncated`
180
+ );
181
+ }
182
+ break;
183
+ }
184
+ }
185
+ return acc;
186
+ }
187
+ var PAGE = 100;
188
+ var OFFSET_CONCURRENCY = 8;
189
+ var OFFSET_CEILING = 1e4;
190
+ async function enumerateByOffset(fetchAt, fallback) {
191
+ const first = await fetchAt(0);
192
+ if (first.count === void 0) return fallback();
193
+ const all = [...first.data];
194
+ const ceiling = Math.min(first.count, OFFSET_CEILING);
195
+ const offsets = [];
196
+ for (let o = PAGE; o < ceiling; o += PAGE) offsets.push(o);
197
+ for (let i = 0; i < offsets.length; i += OFFSET_CONCURRENCY) {
198
+ const pages = await Promise.all(
199
+ offsets.slice(i, i + OFFSET_CONCURRENCY).map(fetchAt)
200
+ );
201
+ for (const page of pages) all.push(...page.data);
202
+ }
203
+ if (first.count > OFFSET_CEILING) {
204
+ console.warn(
205
+ `[sitemap] count ${first.count} exceeds the ${OFFSET_CEILING} offset ceiling \u2014 tail not enumerated (needs cursor/bulk support).`
206
+ );
207
+ }
208
+ return all;
209
+ }
210
+ function enumerateJobs(board) {
211
+ return enumerateByOffset(
212
+ (offset) => board.jobs.list({ limit: PAGE, offset }),
213
+ () => drainPages(paginate(board.jobs.list, { limit: PAGE }).pages())
214
+ );
215
+ }
216
+ function enumerateCompanies(board) {
217
+ return enumerateByOffset(
218
+ (offset) => board.companies.list({ limit: PAGE, offset }),
219
+ () => drainPages(paginate(board.companies.list, { limit: PAGE }).pages())
220
+ );
221
+ }
222
+ async function listedBuckets(board) {
223
+ const { features } = await board.context();
224
+ return SITEMAP_BUCKETS.filter((b) => b !== "blog" || features.blog);
225
+ }
226
+ async function buildBucketUrls(board, origin, bucket) {
227
+ switch (bucket) {
228
+ case "marketing":
229
+ return marketing(board, origin);
230
+ case "jobs-categories":
231
+ return jobsTaxonomy(board, origin, "categories");
232
+ case "jobs-skills":
233
+ return jobsTaxonomy(board, origin, "skills");
234
+ case "jobs-locations":
235
+ return jobsLocations(board, origin);
236
+ case "jobs-details":
237
+ return jobDetails(board, origin);
238
+ case "companies":
239
+ return companies(board, origin);
240
+ case "salaries":
241
+ return salaries(board, origin);
242
+ case "blog":
243
+ return blog(board, origin);
244
+ }
245
+ }
246
+ async function marketing(board, origin) {
247
+ const { features } = await board.context();
248
+ const urls = [
249
+ `${origin}/`,
250
+ `${origin}/jobs`,
251
+ `${origin}/about`,
252
+ `${origin}/privacy-policy`,
253
+ `${origin}/terms-of-service`,
254
+ `${origin}/cookie-policy`
255
+ ];
256
+ if (features.impressum) urls.push(`${origin}/impressum`);
257
+ if (features.talentDirectory) urls.push(`${origin}/talent`);
258
+ if (features.employers) urls.push(`${origin}/employers`);
259
+ return urls;
260
+ }
261
+ async function jobsTaxonomy(board, origin, kind) {
262
+ const jobs = await enumerateJobs(board);
263
+ const counts = /* @__PURE__ */ new Map();
264
+ for (const job of jobs) {
265
+ const slugs = new Set(
266
+ (kind === "categories" ? job.categories : job.skills).map((t) => t.slug)
267
+ );
268
+ for (const slug of slugs) counts.set(slug, (counts.get(slug) ?? 0) + 1);
269
+ }
270
+ const prefix = kind === "categories" ? `${origin}/jobs` : `${origin}/jobs/skills`;
271
+ return [...counts.entries()].filter(([, n]) => n >= MIN_JOBS_PER_INDEXED_PAGE).map(([slug]) => slug).sort().map((slug) => `${prefix}/${slug}`);
272
+ }
273
+ async function jobsLocations(board, origin) {
274
+ const { language } = await board.context();
275
+ const { data } = await board.salaries.locations.list({ locale: language });
276
+ return data.filter((l) => l.jobCount >= MIN_JOBS_PER_INDEXED_PAGE).map((l) => l.placeSlug).sort().map((slug) => `${origin}/jobs/locations/${slug}`);
277
+ }
278
+ async function jobDetails(board, origin) {
279
+ const jobs = await enumerateJobs(board);
280
+ const seen = /* @__PURE__ */ new Set();
281
+ for (const job of jobs) {
282
+ if (!job.company?.slug || !job.slug) continue;
283
+ seen.add(`${origin}/companies/${job.company.slug}/jobs/${job.slug}`);
284
+ }
285
+ return [...seen].sort();
286
+ }
287
+ async function companies(board, origin) {
288
+ const [list, markets] = await Promise.all([
289
+ enumerateCompanies(board),
290
+ board.companies.markets()
291
+ ]);
292
+ const urls = [`${origin}/companies`];
293
+ for (const company of list) urls.push(`${origin}/companies/${company.slug}`);
294
+ for (const market of markets.data) {
295
+ urls.push(`${origin}/companies/markets/${market.slug}`);
296
+ }
297
+ return urls;
298
+ }
299
+ async function salaries(board, origin) {
300
+ const { language } = await board.context();
301
+ const [salaryCompanies, titles, skills, locations] = await Promise.all([
302
+ board.salaries.companies.list(),
303
+ board.salaries.titles.list({ locale: language }),
304
+ board.salaries.skills.list({ locale: language }),
305
+ board.salaries.locations.list({ locale: language })
306
+ ]);
307
+ if (!salaryCompanies.data.length && !titles.data.length && !skills.data.length && !locations.data.length) {
308
+ return [];
309
+ }
310
+ const urls = [`${origin}/salaries`];
311
+ if (salaryCompanies.data.length) {
312
+ urls.push(`${origin}/salaries/companies`);
313
+ for (const c of salaryCompanies.data) {
314
+ urls.push(`${origin}/companies/${c.companySlug}/salaries`);
315
+ }
316
+ }
317
+ if (titles.data.length) {
318
+ urls.push(`${origin}/salaries/titles`);
319
+ for (const t of titles.data)
320
+ urls.push(`${origin}/salaries/titles/${t.slug}`);
321
+ }
322
+ if (skills.data.length) {
323
+ urls.push(`${origin}/salaries/skills`);
324
+ for (const s of skills.data)
325
+ urls.push(`${origin}/salaries/skills/${s.slug}`);
326
+ }
327
+ if (locations.data.length) {
328
+ urls.push(`${origin}/salaries/locations`);
329
+ for (const l of locations.data) {
330
+ urls.push(`${origin}/salaries/locations/${l.placeSlug}`);
331
+ }
332
+ }
333
+ return urls;
334
+ }
335
+ async function blog(board, origin) {
336
+ const posts = await drainPages(
337
+ paginate(board.blog.posts.list, { limit: 100 }).pages()
338
+ );
339
+ const urls = [`${origin}/blog`];
340
+ const tagSlugs = /* @__PURE__ */ new Set();
341
+ const authorSlugs = /* @__PURE__ */ new Set();
342
+ for (const post of posts) {
343
+ urls.push(`${origin}/blog/${post.slug}`);
344
+ for (const tag of post.tags) tagSlugs.add(tag.slug);
345
+ for (const author of post.authors) authorSlugs.add(author.slug);
346
+ }
347
+ for (const slug of [...tagSlugs].sort())
348
+ urls.push(`${origin}/blog/tag/${slug}`);
349
+ for (const slug of [...authorSlugs].sort()) {
350
+ urls.push(`${origin}/blog/author/${slug}`);
351
+ }
352
+ return urls;
353
+ }