Bare z.coerce.number() accepted negative and fractional values for count/pagination flags, and slice(0, limit) with a negative limit silently dropped trailing results instead of erroring. Tightens the schemas to .int().min(1) across all four Danish portal CLIs (including jobnet occupations --per-page) with network-free validation tests. By @ayobamiseun.
jobindex-cli
CLI for searching jobs on Jobindex.dk.
Base URL: https://www.jobindex.dk/
Authentication: None required.
Format: The API returns JSON with embedded HTML blobs. The CLI parses the HTML internally and emits clean JSON.
Installation
cd skills/jobindex-search/cli
bun install
Commands
| Command | Description |
|---|---|
search |
Search for job listings |
detail |
Fetch full detail for a single job listing |
All commands accept --format json|table|plain (default: json).
All errors are written to stderr as { "error": "...", "code": "..." } and the process exits with code 1.
search — Search for job listings
Endpoint: GET https://www.jobindex.dk/jobsoegning.json
bun run src/cli.ts search [flags]
The API always returns 20 results per page (fixed — no --per-page flag). The CLI parses the result_list_box_html HTML blob from the response to extract structured job records.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--query / -q |
string | — | Keyword search query (e.g. python, grafisk designer) |
--page |
number | 1 |
Page number (1-indexed) |
--jobage |
number | 9999 |
Max age of posting in days: 1, 7, 14, 30, or 9999 (all) |
--sort |
string | score |
Sort order: score (relevance) or date (newest first) |
--limit |
number | — | Cap total results returned by the CLI (client-side) |
--format |
string | json |
Output format: json, table, plain |
Sort options
| Value | Description |
|---|---|
score |
Relevance / best match (default) |
date |
Newest postings first |
jobage options
| Value | Description |
|---|---|
1 |
Posted today |
7 |
Last 7 days |
14 |
Last 14 days |
30 |
Last 30 days |
9999 |
All time (default) |
Example
# Search for Python jobs posted in the last 7 days, sorted by date
bun run src/cli.ts search --query python --jobage 7 --sort date
# Search for "grafisk designer" jobs — show first 5 results
bun run src/cli.ts search --query "grafisk designer" --limit 5
# Page 2 of results for data engineer
bun run src/cli.ts search --query "data engineer" --page 2 --format table
Response shape
{
"meta": {
"total": 237,
"page": 1,
"perPage": 20
},
"results": [
{
"id": "h1647303",
"title": "Data Engineer til opbygning af Gavefabrikkens dataplatform",
"company": "Gavefabrikken",
"companyUrl": "https://www.gavefabrikken.dk/",
"location": "Valby",
"date": "2026-03-12",
"url": "https://www.jobindex.dk/jobannonce/h1647303/data-engineer-til-opbygning-af-gavefabrikkens-dataplatform",
"description": "Vi søger en dygtig Data Engineer til at opbygge og vedligeholde vores dataplatform..."
}
]
}
Field notes:
id— string ID prefixed withh(e.g.h1647303). Use this with thedetailcommand.company— company name; may benullfor some aggregated listings.companyUrl— company homepage URL; may benullif not present.location— city or area; may benullif not listed.date— ISO date string (YYYY-MM-DD) from thedatetimeattribute on the<time>element; may benull.description— short excerpt from the listing; may benullor empty.url— full Jobindex.dk URL for the listing.totalinmeta— parsed fromhitcount_html(Danish thousands separator.is stripped before parsing, e.g.18.903→18903).
Note on area filtering: The Jobindex API does not reliably support area/region filtering via query parameters.
areaandgeoareaidparams are silently ignored. To filter by location, use--querywith a city name (e.g.--query "python aarhus") or apply--limitand filter the JSON output externally.
detail — Fetch full job listing detail
URL: https://www.jobindex.dk/jobannonce/{id}/{slug}
bun run src/cli.ts detail <id> [--format json|plain]
The id is the job ID from search results (e.g. h1647303). The slug is optional — the CLI fetches the canonical URL by first constructing https://www.jobindex.dk/jobannonce/{id} and following any redirect, or by using the full URL from the url field in search results.
You may also pass the full URL directly as the id argument.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--format |
string | json |
Output format: json, plain |
Example
# Using ID from search results
bun run src/cli.ts detail h1647303
# Using full URL
bun run src/cli.ts detail "https://www.jobindex.dk/jobannonce/h1647303/data-engineer-til-opbygning-af-gavefabrikkens-dataplatform"
# Plain text output
bun run src/cli.ts detail h1647303 --format plain
Response shape
{
"id": "h1647303",
"title": "Data Engineer til opbygning af Gavefabrikkens dataplatform",
"company": "Gavefabrikken",
"companyUrl": "https://www.gavefabrikken.dk/",
"location": "Valby, København",
"date": "2026-03-12",
"deadline": "2026-04-01",
"employmentType": "Fastansættelse",
"hours": "Fuldtid",
"applyUrl": "https://www.gavefabrikken.dk/jobs/apply/123",
"url": "https://www.jobindex.dk/jobannonce/h1647303/data-engineer-til-opbygning-af-gavefabrikkens-dataplatform",
"description": "Full job description text here..."
}
Field notes:
deadline— application deadline date string;nullif not listed.employmentType— e.g."Fastansættelse","Midlertidig ansættelse";nullif not listed.hours— e.g."Fuldtid","Deltid";nullif not listed.applyUrl— the external application URL (resolved from the Jobindex redirect link/c?t=...);nullif not available.description— full plain-text job description (HTML stripped).- All fields may be
nullif not present in the HTML.
Error handling
All errors are written to stderr in JSON format and exit with code 1:
{ "error": "Job not found", "code": "NOT_FOUND" }
{ "error": "API request failed: 500 Internal Server Error", "code": "API_ERROR" }
{ "error": "Failed to parse job listing HTML", "code": "PARSE_ERROR" }
{ "error": "--query is required", "code": "MISSING_REQUIRED" }
URL construction
Job detail pages on jobindex.dk:
https://www.jobindex.dk/jobannonce/{id}/{slug}
The slug is part of the url returned by search. When calling detail with just an ID, the CLI fetches https://www.jobindex.dk/jobannonce/{id} which redirects to the full URL.
Parsing notes
Total count from hitcount_html
The API returns pagination info as an HTML string like:
<div class="jix_pagination_total"><strong>1</strong> til <strong>20</strong> af <strong>18.903</strong> resultater.</div>
Parse total with: /af <strong>([\d.]+)<\/strong>/ and strip . before converting to integer.
Job card selectors
Each job card is wrapped in [data-beacon-tid]. Inside, select:
| Field | Selector |
|---|---|
id |
[data-beacon-tid] attribute value |
title |
h4 > a text content |
url |
h4 > a[href] |
company |
.jix-toolbar-top__company a text |
companyUrl |
.jix-toolbar-top__company a[href] |
location |
span.jix_robotjob--area text |
date |
time[datetime] attribute value |
description |
p text content (first <p> in card) |
Two card types exist: div.PaidJob (sponsored) and div.jix_robotjob (aggregated). Both use the same selector pattern.