You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TL;DR The public job board at /jobs: a filterable, paginated, live-updating list of all published postings, discoverable for logged-out visitors and search engines, without adding clutter for members who don't care about jobs.
Order: 6/9 in the Jobs & Organizations milestone. Depends on #932. Where:/jobs (the slug is already reserved), new LiveView; Vutuv.Jobs list queries; shell navigation.
LiveView list of published postings, newest first, keyset pagination (UUID v7 ids give a stable tiebreaker). Card: title, employer (verified-organization badge or poster name — same trust UI as Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932), location (city, plus country when it differs from :default_country) or "Remote (DE, AT)", employment-type chip, the salary range (always present per Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932; volunteer postings show "Ehrenamtlich"), age ("vor 3 Tagen"). A signed-in viewer's own tags are highlighted on the card, the same overlap glance as the detail page.
Filters, all URL-shareable (?q=…&tag=…&near=…&radius=…&country=…&salary_min=…): free-text q over title + description (Postgres full-text, like post search), tag, workplace type (onsite/hybrid/remote), employment type. Filter chips mobile-first (horizontal scroll row, like the search page's scope chips); active filters clearable in one tap. Defaults are harmless: no filter active means the plain newest-first board.
Location filter ("Wo?"): one input taking a city name or zip code, plus a radius select (exakt / 10 / 25 / 50 / 100 km) → ?near=Köln&radius=50 or ?near=50667&radius=25. Resolved via Vutuv.Geo (Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932) and matched against posting coordinates; postings without coordinates (unknown zip) fall back to case-insensitive city / exact-zip text match, so nothing silently disappears. A country select covers cross-border regions (someone in Aachen or Basel searches two or three countries) — onsite/hybrid postings match on their address country.
Remote postings answer the candidate's real question, "near me or remote for me": while a location or country filter is active, remote postings stay in the results when their applicant countries include the searched country (their card keeps the "Remote" chip); the workplace-type chips narrow to remote-only or on-site-only from there.
Tags are a first-class filter: tag chips on cards filter the board in one tap, and a signed-in member gets a "Passend zu meinen Tags" chip matching postings that share at least one tag with their profile (required-priority matches rank first). This is vutuv's tag graph doing the actual matchmaking.
PubSub: a posting published/expired/frozen while the board is open appears/disappears live (topic jobs, follow the feed patterns).
Organization pages (/organizations/:slug) get their "Offene Stellen" section: the same card list scoped to the organization, paginated.
Public tag pages get an "Offene Stellen" section too: the same cards scoped to postings carrying the tag, with a link into the pre-filtered board (/jobs?tag=…). Tag pages have agent-format siblings, so their doc builder and drift test learn the section as well.
Empty states that sell the feature ("Noch keine Stellenanzeigen — veröffentliche die erste"), German throughout.
Discoverability without clutter: one "Jobs" entry in the top-bar/burger navigation. No new bottom-tab on mobile, no feed injection, no badges for members who never visit the board. Members who don't care see exactly one menu word more than today.
SEO & GEO: the board is public and indexable (logged-out view = the crawlable view), canonical URL, one h1; noJobPosting JSON-LD on the list (detail pages only — search engines require markup on leaf pages). Agent-format siblings /jobs.md/.txt/.json/.xml (paginated like the member directory's agent docs, listing only postings whose poster left geo? on, each entry carrying the structured location, salary and tag fields so agents can filter client-side) and an /llms.txt entry. The board links every detail page and doubles as the link-walk crawl path for engines that skip the sitemap: add a "Jobs" link to the shared footer (next to the member-directory link in app.html.heex), and render the board's pagination as plain <a href> links (LiveView patch links are fine — they are real hrefs), so every published posting is reachable by following links alone, with the sitemap as the second, faster channel. Docs: extend docs/architecture/jobs.md (board, filters, PubSub topics). Smoke test (browser): publish postings in two cities plus one remote posting allowing DE → /jobs lists all; near=Köln&radius=50 keeps the Cologne posting and the remote one, the remote-only chip narrows further, and the URL is shareable (open it in a second window); set a Mindestgehalt above one posting's maximum → it drops out; sign in as a member with a stored minimum-salary expectation → the salary filter prefills in one tap and the figure appears nowhere in the page source; the "Passend zu meinen Tags" chip keeps only tag-matching postings; open a tag page → its "Offene Stellen" section lists the posting; publish a third posting from another account while the board is open → it appears without reload; expire one via psql + sweeper → it disappears live; check /jobs.md, the German render, and mobile viewport (filters usable at 390px). Out of scope: saved searches + alerts (8/9), admin views (7/9), a jobs scope on the global /search page (can join later once the board proves the filter set).
TL;DR The public job board at
/jobs: a filterable, paginated, live-updating list of all published postings, discoverable for logged-out visitors and search engines, without adding clutter for members who don't care about jobs.Order: 6/9 in the Jobs & Organizations milestone. Depends on #932.
Where:
/jobs(the slug is already reserved), new LiveView;Vutuv.Jobslist queries; shell navigation.Want:
everyone/members, Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932) and the exclusion list (Jobs: Per-posting exclusion list for job offers (hide a posting from competitors, your own staff, or specific people) #939): the board, its filters, search, agent formats and PubSub updates only ever surface a posting the current viewer may see. Amembersposting never appears to logged-out visitors, in the crawlable/JSON-LD/sitemap surfaces, or in/jobs.mdetc.; aneveryoneposting excluded for this signed-in viewer (per the Jobs: Per-posting exclusion list for job offers (hide a posting from competitors, your own staff, or specific people) #939 seam — own list ∪ organization default ∪ block) is filtered out for them exactly as if it did not exist. Fold both into the base list query so no downstream filter can leak a hidden posting.publishedpostings, newest first, keyset pagination (UUID v7 ids give a stable tiebreaker). Card: title, employer (verified-organization badge or poster name — same trust UI as Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932), location (city, plus country when it differs from:default_country) or "Remote (DE, AT)", employment-type chip, the salary range (always present per Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932; volunteer postings show "Ehrenamtlich"), age ("vor 3 Tagen"). A signed-in viewer's own tags are highlighted on the card, the same overlap glance as the detail page.?q=…&tag=…&near=…&radius=…&country=…&salary_min=…): free-textqover title + description (Postgres full-text, like post search), tag, workplace type (onsite/hybrid/remote), employment type. Filter chips mobile-first (horizontal scroll row, like the search page's scope chips); active filters clearable in one tap. Defaults are harmless: no filter active means the plain newest-first board.?near=Köln&radius=50or?near=50667&radius=25. Resolved viaVutuv.Geo(Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932) and matched against posting coordinates; postings without coordinates (unknown zip) fall back to case-insensitive city / exact-zip text match, so nothing silently disappears. Acountryselect covers cross-border regions (someone in Aachen or Basel searches two or three countries) — onsite/hybrid postings match on their address country.?salary_min=60000keeps postings whose salary reaches the figure (yearly-normalizedsalary_max, same-currency only — the Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932 comparison rule). A signed-in member with a stored minimum-salary expectation (Jobs 1/9: Employment-status visibility + minimum-salary expectation #928) gets a one-tap "ab meiner Gehaltsvorstellung" prefill; the stored figure itself is never rendered, even at visibilityhidden— it only parameterizes their own filter.jobs, follow the feed patterns)./organizations/:slug) get their "Offene Stellen" section: the same card list scoped to the organization, paginated./jobs?tag=…). Tag pages have agent-format siblings, so their doc builder and drift test learn the section as well.SEO & GEO: the board is public and indexable (logged-out view = the crawlable view), canonical URL, one
h1; noJobPostingJSON-LD on the list (detail pages only — search engines require markup on leaf pages). Agent-format siblings/jobs.md/.txt/.json/.xml(paginated like the member directory's agent docs, listing only postings whose poster leftgeo?on, each entry carrying the structured location, salary and tag fields so agents can filter client-side) and an/llms.txtentry. The board links every detail page and doubles as the link-walk crawl path for engines that skip the sitemap: add a "Jobs" link to the shared footer (next to the member-directory link inapp.html.heex), and render the board's pagination as plain<a href>links (LiveView patch links are fine — they are real hrefs), so every published posting is reachable by following links alone, with the sitemap as the second, faster channel.Docs: extend
docs/architecture/jobs.md(board, filters, PubSub topics).Smoke test (browser): publish postings in two cities plus one remote posting allowing DE →
/jobslists all;near=Köln&radius=50keeps the Cologne posting and the remote one, the remote-only chip narrows further, and the URL is shareable (open it in a second window); set a Mindestgehalt above one posting's maximum → it drops out; sign in as a member with a stored minimum-salary expectation → the salary filter prefills in one tap and the figure appears nowhere in the page source; the "Passend zu meinen Tags" chip keeps only tag-matching postings; open a tag page → its "Offene Stellen" section lists the posting; publish a third posting from another account while the board is open → it appears without reload; expire one via psql + sweeper → it disappears live; check/jobs.md, the German render, and mobile viewport (filters usable at 390px).Out of scope: saved searches + alerts (8/9), admin views (7/9), a jobs scope on the global
/searchpage (can join later once the board proves the filter set).