Initial release: AI-powered job application framework

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Job Search
2026-03-23 08:34:06 +01:00
committed by Mads Lorentzen
co-authored by Claude Opus 4.6
commit c66d599d75
73 changed files with 6539 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
---
name: jobindex-search
version: 1.0.0
description: >
Make sure to use this skill whenever the user wants to search for jobs in Denmark,
find Danish job listings, look up a specific job posting, or asks anything about
the Danish job market — even if they don't mention jobindex.dk explicitly. Invoke
this skill for questions about open positions, job vacancies, hiring in Denmark,
job opportunities in Danish cities or sectors, or when the user wants to find work
in Denmark. Also trigger for phrases like "find me a job", "are there any jobs for
X in Copenhagen", or "what jobs are available in Aarhus" when the context is Denmark.
Trigger phrases include: jobindex, jobsøgning, job i Danmark, ledige stillinger,
job opslag, find job, stillingopslag, jobannonce, job vacancy denmark, danish jobs,
jobs in denmark, job search denmark, work in denmark, find work denmark, IT jobs
denmark, engineer jobs denmark, developer jobs copenhagen, marketing jobs aarhus,
jobs aarhus, jobs copenhagen, jobs odense, jobs aalborg, job openings denmark,
hiring denmark, job listings denmark, python jobs denmark, grafisk designer job,
data engineer job, softwareudvikler job, full stack developer job danmark.
context: fork
allowed-tools: Bash(bun run skills/jobindex-search/cli/src/cli.ts *)
---
# Jobindex Search Skill
Search live Danish job listings from Jobindex.dk. No authentication needed.
Covers thousands of job postings across all sectors, updated in real time.
## When to use this skill
Invoke this skill when the user wants to:
- Search for job openings in Denmark by keyword, job title, or technology
- Find jobs in a specific Danish city (use keyword with city name, e.g. `python aarhus`)
- Filter jobs by recency (posted today, last 7 days, last 30 days)
- Get the full description of a specific job listing
- Explore the Danish job market for a given profession or skill set
## Commands
### Search job listings
```bash
bun run skills/jobindex-search/cli/src/cli.ts search [flags]
```
Key flags:
- `--query <text>` / `-q <text>` — keyword search (job title, skill, company, city). **Required** for meaningful results.
- `--jobage <days>` — filter by posting age: `1` (today), `7`, `14`, `30`, or `9999` (all, default)
- `--sort <order>``score` (relevance, default) or `date` (newest first)
- `--page <n>` — page number (1-indexed, 20 results per page, fixed)
- `--limit <n>` — cap total results the CLI outputs (client-side)
- `--format json|table|plain`
> **Area note**: The Jobindex API does not support area filtering via params. To find jobs in a specific city, include the city in `--query` (e.g. `--query "data engineer københavn"` or `--query "python aarhus"`).
### Fetch full job detail
```bash
bun run skills/jobindex-search/cli/src/cli.ts detail <id> [--format json|plain]
```
`id` is the job ID from `search` results (e.g. `h1647303`). You may also pass the full Jobindex URL. Returns the full job description, deadline, employment type, hours, and apply link.
---
## How to use effectively
**Always start with `search`.** Pass the job title, skill, or profession as `--query`. Combine with a city name in the query to narrow by location (e.g. `--query "frontend developer odense"`).
**Use `--jobage 7` or `--jobage 1` for fresh listings.** Without it, results include all historical postings.
**Use `--sort date` to see the most recently posted jobs first.** Default `score` sorts by relevance.
**Natural workflow: `search` → `detail`.**
1. Use `search` to find matching jobs and their `id` values.
2. Call `detail <id>` to get the full description, deadline, and apply link.
**Use `--format table` for quick scanning**, `--format json` for data processing, and `--format plain` for reading a single job's full details.
**Pagination**: The API always returns 20 results per page. Use `--page` to navigate pages. Use `--limit` to cap results across one page fetch.
---
## Usage examples
### Find Python jobs posted in the last 7 days
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--query python \
--jobage 7 \
--sort date \
--format table
```
### Data engineer jobs in Copenhagen
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--query "data engineer københavn" \
--sort score \
--format table
```
### Graphic designer jobs — all time, by relevance
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--query "grafisk designer" \
--limit 10 \
--format table
```
### Full-stack developer jobs, page 2
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--query "full stack developer" \
--page 2 \
--format json
```
### Jobs posted today across all sectors
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--jobage 1 \
--sort date \
--limit 20 \
--format table
```
### Get full details for a specific job
```bash
bun run skills/jobindex-search/cli/src/cli.ts detail h1647303 --format plain
```
### Marketing jobs in Aarhus
```bash
bun run skills/jobindex-search/cli/src/cli.ts search \
--query "marketing aarhus" \
--jobage 30 \
--sort date \
--format table
```
---
## Output formats
| Format | Best for |
|--------|----------|
| `json` | Default — programmatic use, data processing, passing IDs to `detail` |
| `table` | Quick human-readable overview and scanning |
| `plain` | Reading a single job's full detail (`detail` command) |
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1`.
---
## Notes
- All data is from the public `jobindex.dk` API — no credentials required.
- Page size is fixed at 20 results per page (Jobindex API limitation).
- Area/region filtering via API params does not work — include city names in `--query` instead.
- `--jobage 9999` is the default and includes all postings regardless of age.
- Total count in `meta.total` uses Danish dot-thousands notation internally (e.g. `18.903`) — the CLI normalizes this to a plain integer.
- Job IDs are string-prefixed (e.g. `h1647303`) — pass them as-is to `detail`.
@@ -0,0 +1,230 @@
# jobindex-cli
CLI for searching jobs on [Jobindex.dk](https://www.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
```bash
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`
```bash
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
```bash
# 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
```json
{
"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 with `h` (e.g. `h1647303`). Use this with the `detail` command.
- `company` — company name; may be `null` for some aggregated listings.
- `companyUrl` — company homepage URL; may be `null` if not present.
- `location` — city or area; may be `null` if not listed.
- `date` — ISO date string (`YYYY-MM-DD`) from the `datetime` attribute on the `<time>` element; may be `null`.
- `description` — short excerpt from the listing; may be `null` or empty.
- `url` — full Jobindex.dk URL for the listing.
- `total` in `meta` — parsed from `hitcount_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. `area` and `geoareaid` params are silently ignored. To filter by location, use `--query` with a city name (e.g. `--query "python aarhus"`) or apply `--limit` and filter the JSON output externally.
---
## `detail` — Fetch full job listing detail
**URL**: `https://www.jobindex.dk/jobannonce/{id}/{slug}`
```bash
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
```bash
# 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
```json
{
"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; `null` if not listed.
- `employmentType` — e.g. `"Fastansættelse"`, `"Midlertidig ansættelse"`; `null` if not listed.
- `hours` — e.g. `"Fuldtid"`, `"Deltid"`; `null` if not listed.
- `applyUrl` — the external application URL (resolved from the Jobindex redirect link `/c?t=...`); `null` if not available.
- `description` — full plain-text job description (HTML stripped).
- All fields may be `null` if not present in the HTML.
---
## Error handling
All errors are written to **stderr** in JSON format and exit with code `1`:
```json
{ "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:
```html
<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.
@@ -0,0 +1,25 @@
{
"name": "jobindex-cli",
"version": "1.0.0",
"description": "CLI for searching jobs on Jobindex.dk",
"type": "module",
"main": "src/cli.ts",
"bin": {
"jobindex": "src/cli.ts"
},
"scripts": {
"start": "bun run src/cli.ts",
"test": "bun test --timeout 30000",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@bunli/core": "latest",
"@bunli/utils": "latest",
"node-html-parser": "^6.1.13",
"zod": "^3.23.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/bun": "latest"
}
}
@@ -0,0 +1,14 @@
import { createCLI } from "@bunli/core"
import { search } from "./commands/search.js"
import { detail } from "./commands/detail.js"
const cli = await createCLI({
name: "jobindex-cli",
version: "0.1.0",
description: "CLI for searching jobs on Jobindex.dk",
})
cli.command(search)
cli.command(detail)
await cli.run()
@@ -0,0 +1,197 @@
export const BASE_URL = "https://www.jobindex.dk"
export function writeError(error: string, code: string): void {
process.stderr.write(JSON.stringify({ error, code }) + "\n")
}
export async function apiFetch<T>(path: string, params?: Record<string, string>): Promise<T> {
let url = `${BASE_URL}${path}`
if (params && Object.keys(params).length > 0) {
const qs = new URLSearchParams(params)
url += `?${qs.toString()}`
}
const maxRetries = 6
let delay = 500
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url)
if (response.status === 429 || response.status >= 500) {
if (attempt === maxRetries) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
const jitter = Math.floor(Math.random() * 500)
await new Promise((resolve) => setTimeout(resolve, delay + jitter))
delay = Math.min(delay * 2, 5000)
continue
}
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
return response.json() as Promise<T>
}
throw new Error("API request failed after max retries")
}
export async function htmlFetch(url: string): Promise<string> {
const maxRetries = 6
let delay = 500
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; jobindex-cli/1.0)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "da,en;q=0.9",
},
redirect: "follow",
})
if (response.status === 429 || response.status >= 500) {
if (attempt === maxRetries) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
const jitter = Math.floor(Math.random() * 500)
await new Promise((resolve) => setTimeout(resolve, delay + jitter))
delay = Math.min(delay * 2, 5000)
continue
}
if (response.status === 404) {
throw new Error(`Job not found`)
}
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
return response.text()
}
throw new Error("Request failed after max retries")
}
export interface JobCard {
id: string
title: string
company: string | null
companyUrl: string | null
location: string | null
date: string | null
url: string
description: string | null
}
/**
* Decode HTML entities in text
*/
function decodeHtmlEntities(text: string): string {
return text
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
.replace(/&nbsp;/g, " ")
}
/**
* Strip HTML tags from text
*/
function stripTags(html: string): string {
return html.replace(/<[^>]+>/g, "").trim()
}
/**
* Parse job cards from result_list_box_html using regex.
* node-html-parser has nesting bugs with this specific HTML structure
* (unclosed tags inside buttons cause incorrect DOM tree).
* Regex parsing is more reliable for this specific HTML format.
*/
export function parseJobCards(html: string): JobCard[] {
const results: JobCard[] = []
// Split HTML by jobad-wrapper to get individual card HTML chunks
const wrapperPattern = /<div[^>]+id="jobad-wrapper-(h\d+|r\d+)"[^>]*>([\s\S]*?)(?=<div[^>]+id="jobad-wrapper-|$)/g
let match: RegExpExecArray | null
while ((match = wrapperPattern.exec(html)) !== null) {
const id = match[1]
const cardHtml = match[2]
// Extract title: look for <h4>...<a|A href="...">Title</a>...</h4>
const titleMatch = cardHtml.match(/<h4[^>]*>[\s\S]*?<[Aa][^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/[Aa]>/i)
if (!titleMatch) continue
const rawTitle = stripTags(titleMatch[2])
const title = decodeHtmlEntities(rawTitle)
if (!title) continue
// Determine URL: prefer jobindex.dk /jobannonce/ URL, fallback to constructed URL
let url: string
const jobannonce = cardHtml.match(/href="(https:\/\/www\.jobindex\.dk\/jobannonce\/[^"]+)"/)
if (jobannonce) {
url = jobannonce[1]
} else {
// Construct canonical URL from ID
url = `${BASE_URL}/jobannonce/${id}`
}
// Extract company: <a ...> inside jix-toolbar-top__company
let company: string | null = null
let companyUrl: string | null = null
const companySection = cardHtml.match(/class="jix-toolbar-top__company"[^>]*>([\s\S]*?)<\/div>/i)
if (companySection) {
const companyLinkMatch = companySection[1].match(/<[Aa][^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/[Aa]>/i)
if (companyLinkMatch) {
company = decodeHtmlEntities(stripTags(companyLinkMatch[2])) || null
companyUrl = companyLinkMatch[1] || null
}
}
// Extract location: <span class="jix_robotjob--area">Location</span>
const locMatch = cardHtml.match(/<span[^>]+class="jix_robotjob--area"[^>]*>([\s\S]*?)<\/span>/i)
const location = locMatch ? decodeHtmlEntities(stripTags(locMatch[1])) || null : null
// Extract date: <time datetime="YYYY-MM-DD">
const dateMatch = cardHtml.match(/<time[^>]+datetime="([^"]+)"/)
const date = dateMatch ? dateMatch[1] : null
// Extract description: first <p class="..."> or first standalone <p> (not in toolbar)
// Skip the toolbar/menu section and look for the description paragraph
let description: string | null = null
const innerSection = cardHtml.match(/class="PaidJob-inner"[^>]*>([\s\S]*?)(?:<\/div>\s*<\/div>|$)/i) ||
cardHtml.match(/class="jix_robotjob-inner"[^>]*>([\s\S]*?)(?:<\/div>\s*<\/div>|$)/i)
if (innerSection) {
const pMatch = innerSection[1].match(/<p[^>]*>([\s\S]*?)<\/p>/i)
if (pMatch) {
const text = decodeHtmlEntities(stripTags(pMatch[1]))
description = text.length > 0 ? text.substring(0, 300) : null
}
} else {
// Fallback: look for p after the jobannonce link
const pMatches = [...cardHtml.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)]
for (const pm of pMatches) {
const text = decodeHtmlEntities(stripTags(pm[1]))
if (text.length > 20) {
description = text.substring(0, 300)
break
}
}
}
results.push({
id,
title,
company: company || null,
companyUrl: companyUrl || null,
location: location || null,
date: date || null,
url,
description: description || null,
})
}
return results
}
export function parseHitCount(html: string): number {
const match = html.match(/af <strong>([\d.]+)<\/strong>/)
if (!match) return 0
const numStr = match[1].replace(/\./g, "")
return parseInt(numStr, 10) || 0
}
@@ -0,0 +1,39 @@
import { join } from "path";
const CLI_PATH = join(import.meta.dir, "../src/cli.ts");
export interface CLIResult {
stdout: string;
stderr: string;
exitCode: number;
}
export async function runCLI(args: string[]): Promise<CLIResult> {
const proc = Bun.spawn(["bun", "run", CLI_PATH, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
}
export function parseJSON<T = unknown>(result: CLIResult): T {
if (result.exitCode !== 0) {
throw new Error(
`CLI exited with code ${result.exitCode}. stderr: ${result.stderr}`
);
}
try {
return JSON.parse(result.stdout) as T;
} catch {
throw new Error(
`Failed to parse JSON. stdout: ${result.stdout}\nstderr: ${result.stderr}`
);
}
}
@@ -0,0 +1,118 @@
# Jobindex URL Reference
## Base URL
```
https://www.jobindex.dk/jobsoegning
```
## Full URL Pattern
```
https://www.jobindex.dk/jobsoegning/{category-group}/{category-slug}/{area-slug}?q={query}&jobage={days}&page={num}
```
All path segments and query params are optional. Category MUST come before area in the path.
## Path Segments
### Areas (geography)
Area slug goes at the end of the path:
| Area | Slug |
|------|------|
| Storkøbenhavn | `storkoebenhavn` |
| Københavnsområdet | `storkoebenhavn` |
| Nordsjælland | `nordsjaelland` |
| Sjælland | `sjaelland` |
| Fyn | `fyn` |
| Nordjylland | `nordjylland` |
| Midtjylland | `midtjylland` |
| Sydjylland | `sydjylland` |
| Bornholm | `bornholm` |
**Note:** If the exact slug is unknown, use the Geografi filter UI instead:
1. Click the "Geografi" button
2. Click "Tilføj område"
3. Type city/region name in the textbox
4. Select from autocomplete (treeitem)
5. Click "Vis X job" button
6. Note the URL path that results
### Categories
Category uses a two-part path: `{group}/{slug}`:
| Category | Path |
|----------|------|
| IT-drift og support | `it/itdrift` |
**Note:** Category slugs are not fully mapped. To discover a category slug:
1. Click the "Kategorier" button
2. Type category name in the search field
3. Select from autocomplete (treeitem)
4. Click "Vis X job" button
5. Note the URL path that results
## Query Parameters
| Parameter | Description | Examples |
|-----------|-------------|----------|
| `q` | Search query | `q=data+engineer` (broad), `q=%27data+engineer%27` (exact match) |
| `jobage` | Max age in days | `jobage=1` (today), `jobage=3`, `jobage=7`, `jobage=30` |
| `page` | Page number (1-indexed) | `page=1`, `page=2` |
## Examples
```bash
# Basic keyword search
playwright-cli goto "https://www.jobindex.dk/jobsoegning?q=python+developer"
# Exact match search (single quotes around query)
playwright-cli goto "https://www.jobindex.dk/jobsoegning?q=%27python+developer%27"
# Search in Storkøbenhavn area
playwright-cli goto "https://www.jobindex.dk/jobsoegning/storkoebenhavn?q=python+developer"
# Search with category + area
playwright-cli goto "https://www.jobindex.dk/jobsoegning/it/itdrift/storkoebenhavn?q=data+engineer"
# Last 7 days only
playwright-cli goto "https://www.jobindex.dk/jobsoegning?q=data+engineer&jobage=7"
# Page 2 of results
playwright-cli goto "https://www.jobindex.dk/jobsoegning?q=data+engineer&page=2"
# Everything combined
playwright-cli goto "https://www.jobindex.dk/jobsoegning/storkoebenhavn?q=data+engineer&jobage=7&page=1"
```
## Filters Available via UI Only
These filters require clicking through the filter panel (not URL-constructable):
**Ansættelsestype (Employment type):**
Fastansættelse, Tidsbegrænset, Studiejob, Graduate/trainee, Freelance, etc.
**Arbejdstid (Working hours):**
Fuldtid, Deltid
**Hjemmearbejde (Remote work):**
Muligt, Tilbydes ikke, 100% hjemmearbejde
To use these: click "Filtre" button → check desired options → click "Vis X job".
## Job Card Extraction
CSS selectors for job cards: `div.PaidJob, div.jix_robotjob`
Each card contains:
- Title: `h4 a` (text + href)
- Company: first `a` link text
- Location: div after h4 (clean by removing "Se rejsetid")
- Posted date: `time` element (format: DD-MM-YYYY)
- Description: `p` elements
- Job URL: `h4 a` href (may be external or jobindex-hosted)
~20 results per page.