為您的協會、出版物、社群或企業建立一個 AI 驅動的工作板軟體。讓它成為您的專屬,自動加入相關職缺,並持續成長。
AI 翻譯
Create an AI-powered job board for your association, publication, community, or business. Make it your own, add relevant jobs automatically, and grow.
商業TypeScript · JavaScriptGitHub @wollemiahq上線中營運者未確認
試用過了嗎? 點一下就好,不需要登入。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Cavuno 驅動的工作板入門範本,使用 TanStack Start 與 shadcn/ui 建置。
AI 翻譯
Cavuno-powered job board starter template built with TanStack Start and shadcn/ui.
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
TanStack Start + shadcn/ui job board template — powered by Cavuno
Clone it, pnpm dev, and in under a minute you have a populated,
production-realistic job board — real jobs and companies, a working
applicant tracker, captured outbound emails, and Stripe test-mode checkout.
No API key, no account, and no empty database to seed first.
Built on TanStack Start,
Cloudflare Workers,
shadcn/ui on Base UI,
Tailwind CSS, Geist, and
Lucide. Data comes from Cavuno
via the Cavuno SDK (@cavuno/board), so every
surface is a real page wired to a real backend — not a mock.檢視完整 README ↗Clone it, pnpm dev, and in under a minute you have a populated,
production-realistic job board — real jobs and companies, a working
applicant tracker, captured outbound emails, and Stripe test-mode checkout.
No API key, no account, and no empty database to seed first.
Built on TanStack Start,
Cloudflare Workers,
shadcn/ui on Base UI,
Tailwind CSS, Geist, and
Lucide. Data comes from Cavuno
via the Cavuno SDK (@cavuno/board), so every
surface is a real page wired to a real backend — not a mock.
Most job board templates are a beautiful frontend with no real backend behind it — the listings are hardcoded or mocked, and to actually launch you either build the backend yourself (auth, applications, payments, emails, search) or bolt on a separate service. This one is wired to a real backend from the first clone, and it boots populated so you can see and build every real state immediately. Two things make that work:
.dev.vars.example already points at the platform sandbox board — a live,
deterministic fixture tenant that resets nightly and is safe to write to. So a
fresh clone boots straight into a full board with:
letter_opener-style viewer over every email
the board would send (magic-link sign-in, verification, password reset,
alert double-opt-in). The sandbox captures instead of delivering, so every
flow that "continues in your inbox" is completable on the spot.4242 4242 4242 4242 with no Stripe account or keys of your own.The full sandbox playbook — persona roster, feature-flag toggles, reseed, and
the headless server-function equivalents — is in
docs/preview-states.md.
AGENTS.md is a machine-readable contract for coding agents
working in this repo. It works because the codebase is layered so that
correctness can't be redesigned away:
@cavuno/board SDK: formatters, path helpers,
breadcrumbs, copy. The correctness functions.src/board/**): pure functions
(toJobCardVM, toJobDetailVM, …) that call the SDK and hand components
plain, resolved data.src/components/**): dumb, typed-props
components you can restructure freely without ever mis-calling a formatter.The pinned SDK also ships version-matched Cavuno Agent Skills for Codex,
Claude Code, Cursor, and other compatible coding agents. This starter checks
in the Claude-compatible copy under .claude/skills; the same corpus can be
installed or refreshed in any existing compatible agent skills directory with:
npx @cavuno/board setup
The skills cover the complete board surface—including TanStack Start and
Cloudflare wiring, auth and session ownership, jobs, companies, salaries,
applications, messaging, SEO, i18n, theming, and runtime verification—so an
agent works from the contracts for the exact SDK version installed here rather
than stale generic documentation. cavuno-board doctor also detects when the
checked-in skills no longer match that version.
That separation makes this an agent-buildable job board template: an agent can redesign the surface without breaking salary math, canonical URLs, auth and session ownership, or SEO.
Homepage text can use runtime copy templates with the page's existing data, without additional API requests.
git clone https://github.com/wollemiahq/cavuno-tanstack-start-shadcn-job-board-template
cd cavuno-tanstack-start-shadcn-job-board-template
cp .dev.vars.example .dev.vars # already points at the live sandbox board
pnpm install
pnpm dev # http://localhost:3000
That's it. No keys, no account, no database. The dev server boots on the sandbox board and the preview toolbar appears bottom-left — start switching personas.
Requires pnpm 11 (pinned in
package.json) and Node 24 (matches CI).
Known dev-only papercut: if the very first request to a freshly booted
pnpm devis/embed/jobs, it answers 500 ("Something went wrong") and then works on every request after. Refresh once. Any other route hit first also avoids it. This is upstream — Vite's on-demand SSR module evaluation handscreateSsrRpca not-yet-callablegetServerFnById, so the root shell'sgetBoardContext()throws before the page renders (TanStack/router#6451, #7459)./embed/jobsis the only route that trips it, because it is the only one whose loader callsgetBoardContext()concurrently with the root shell's own call. Production is unaffected — Rollup resolves the chain statically at build time, sopnpm build && pnpm previewserves it 200 on a cold first hit.
The workflow: build and style your board against the sandbox — it's
populated, safe to write to, and resets nightly — then go live by swapping one
value, CAVUNO_BOARD, for your own board's pk_… publishable key (.dev.vars
in dev, wrangler.jsonc vars in production). The app code is identical either
way; you're just pointing it at your real board instead of the sandbox.
The preview toolbar is sandbox-only. Personas, the captured-email viewer, reseed, and the flag toggles are gated on the board being the platform sandbox (
sandbox: true), so they exist purely for exploring and developing the template. PointCAVUNO_BOARDat your own board and the toolbar simply isn't there — you're now developing against your real board, with real users, real sessions, and real outbound email. That gate is board truth from the API, not an env flag, so the toolbar can never render on a production board.
| Variable | What | Where |
|---|---|---|
CAVUNO_API_URL | Board API base URL (https://api.cavuno.com) | .dev.vars / wrangler.jsonc |
CAVUNO_BOARD | Your board's pk_… publishable key (Dashboard → Settings → API) | .dev.vars / wrangler.jsonc |
The pk_… key is client-safe by design; user sessions live in a
host-owned httpOnly cookie this app manages itself. The Board API is only ever
called server-side (src/server/**).
The sandbox exists so you can see every state without seeding anything:
employer-admin. You are
now signed in as a workspace admin.4242 4242 4242 4242. No Stripe setup.
Every product surface is a real, SSR-rendered page wired to the Board API. Feeds, metadata, redirects, and other machine endpoints ship alongside them:
| Surface | Route(s) |
|---|---|
| Home (company discovery + latest jobs) | / |
| Jobs search (filters + master/detail) | /jobs, /jobs/locations |
| Job detail (meta + Google for Jobs JSON-LD) | /companies/:companySlug/jobs/:jobSlug |
| Programmatic SEO job listings | /jobs/:keyword, /jobs/skills/:skill, /jobs/locations/:location, /jobs/locations/:location/:keyword, /jobs/locations/:location/skills/:skill |
| Companies, company jobs, and company salaries | /companies, /companies/:companySlug, /companies/markets/:market, /companies/:companySlug/jobs, /companies/:companySlug/salaries, /companies/:companySlug/salaries/:categorySlug |
| Salaries hubs | /salaries, /salaries/companies, /salaries/titles, /salaries/skills, /salaries/locations |
| Salary title and skill explorers | /salaries/titles/:slug, /salaries/titles/:slug/locations, /salaries/titles/:slug/:locationSlug, /salaries/skills/:slug, /salaries/skills/:slug/locations, /salaries/skills/:slug/:locationSlug |
| Salary location explorers | /salaries/locations/:slug, /salaries/locations/:slug/titles, /salaries/locations/:slug/skills |
| Talent directory and profiles | /talent, /p/:handle |
| Blog, tags, and authors | /blog, /blog/:postSlug, /blog/tag/:tagSlug, /blog/author/:authorSlug |
| Candidate and employer authentication | /auth/sign-in, /auth/sign-up, /auth/employer/sign-up, /auth/join, /auth/magic-link, /auth/oauth-complete, /auth/forgot-password, /auth/reset-password, /auth/verify-email, /auth/verify-email-required, |
The same supported surfaces stay unprefixed by default (English). Extra
chrome locales (/de/…, /fr/…) are opt-in — pnpm locale:add de.
Cross-cutting capabilities that ship on top of those routes:
JobPosting JSON-LD (Google for Jobs),
sitemap.xml, robots.txt, job and blog RSS feeds, and OpenGraph images.
Job-detail URLs mirror Cavuno's hosted board, so a board migrating hosted →
headless keeps its indexed URLs.3 + N, missing logos fall back to initials.pnpm locale:add nl enables Dutch (or use another catalog's locale code).
Run pnpm gen:messages && pnpm gen:paraglide to compile; the footer
switcher then appears. See localization for Dutch
terminology and verification. Deliberately
chrome-only: the UI localizes while board content (jobs, companies) stays
in the board's own language, matching the platform's single-language
board model..dark class, and accessibility from
Base UI semantics and the owned components' explicit ARIA contracts.| Layer | Choice |
|---|---|
| Framework | TanStack Start (SSR) on React 19 |
| Runtime | Cloudflare Workers |
| Build | Vite+ (vp) |
| UI | shadcn/ui on Base UI, Tailwind CSS 4, Geist, Lucide |
| Backend | Cavuno — the hosted job board platform |
| Data | Cavuno SDK (@cavuno/board) |
| i18n | Paraglide JS |
Exact versions are in package.json. @cavuno/board and
vite-plus are pinned exact on purpose — npm's min-release-age policy
would otherwise silently resolve @latest down to an older version.
board.context().ads supplies enabled, clientId, and defaultSlotId.
The starter uses the board's default unit for manual placements. Missing or
invalid IDs render nothing, and the advertising switch and cookie-consent gate
apply even to custom slot IDs. Older APIs without defaultSlotId remain usable;
manual placements stay off until the updated API is deployed.
See placement decisions for the size and UX rationale.
Manual units scroll with the page. To enable a Google-managed bottom anchor,
configure AdSense → Ads → your site: Auto ads on, Anchor ads on, Bottom only,
Allow dynamic anchors off, and desktop anchors on. Disable other Auto ads formats
unless wanted. The starter does not set data-overlays or render a footer placeholder.
Use AdSense page exclusions for private/utility routes and mobile Apply conflicts:
a script already loaded can remain active during client navigation.
The shared BoardAdsProvider supplies defaults. Customize any manual unit in
frontend code without introducing a server-side placement map:
<BoardAdSlot placement="custom-sidebar" layout="rectangle" />
<BoardAdSlot placement="custom-sidebar" layout="rectangle" slotId="1234567890" />
src/ads.json remains a backward-compatible local override: an explicit entry
wins over the default, including a disabled entry. New customizations can pass
slotId directly. The second outer search rail remains opt-in via this file.
Hosted working previews automatically show manual ad placeholders and never request live ads, even when placeholders are hidden.
In development or sandbox preview, open the preview toolbar → Board settings → Ad placements. Placeholders read AD UNIT HERE, issue no ad requests, and persist for the browser tab. Only manual placements are previewed; Google-managed anchors must be checked on the deployed site. Preview does not remove third-party scripts already loaded before it was enabled.
When board.context().ads.googleConsentMessage is true (AdSense on with a
valid publisher id, and the owner has not turned it off), AdSense loads on page
load on every public route (ad pages or not) for every visitor and viewport, so
Google's certified consent message
(AdSense → Privacy & messaging → European regulations) can ask visitors in the
EEA, UK and Switzerland. For those visitors (gdprApplies: true from
__tcfapi) the board's banner never shows; GA4, GTM, Meta, LinkedIn and Cavuno
Analytics load only when the visitor consents to all of the owner's own-use
purposes 1, 7, 8 and 9 (set them to Consent under "Add purposes for your
own use"), and Cookie preferences reopens Google's message. Everyone else,
and anyone whose browser blocks AdSense (at once) or whose CMP has not answered
within 3 seconds, gets the board's banner as before. A decline in Google's
message also stops Cavuno Analytics in the visitor's other open tabs. See
src/lib/google-tcf.ts.
In this mode the AdSense tag loads on every route so Google's consent message works everywhere; manual ad slots still render only on ad pages. With Auto ads or anchor ads on, Google may place ads on any page. If you want to keep ads off pages such as sign-in or checkout, you can add AdSense page exclusions (optional). See placement decisions.
Board analytics IDs (gtmId, ga4MeasurementId, metaPixelId,
linkedInPartnerId, optional LinkedIn conversion IDs) come from
board.context().analytics. The starter supports two operator paths —
use one destination per vendor to avoid double-counting:
| Path | When to use |
|---|---|
| Google Tag Manager | One container ID; wire GA4, Meta, LinkedIn, Google Ads, and custom tags inside GTM |
| Direct Meta / LinkedIn | Pixel ID and/or LinkedIn Partner ID only — native fbq / lintrk at the same moments |
GTM loads on public pages and auth/onboarding routes (/auth/*,
verify-email, resume onboarding). When cookie consent is required, conversion
events still push to window.dataLayer immediately; GTM and pixels load only
after accept, then flush any queued pixel calls.
dataLayer conversion eventsUse these as Custom Event triggers in GTM (field names match hosted Cavuno boards — operator docs):
| Event | When | Key fields |
|---|---|---|
sign_up | Account created (password/OAuth/magic-link). Password: auto-redirect to verify-email landing with cavuno_auth*, not after OTP | method, board_slug |
login | Successful sign-in | method, board_slug |
apply_click | Apply flow opened (not the registration wall) | job_id, job_slug, company_slug, apply_type, board_slug |
apply_submit | Native application submitted | job_id, application_id, job_slug, company_slug, board_slug |
job_alert_subscribe | Job alert confirmed (double opt-in) | board_slug, source (confirm) |
Auth methods: password, google, linkedin, magic_link.
Make this board your own: change its layout, visual identity, navigation, and features while keeping the integrations and behavior your users rely on.
AGENTS.md — concise guidance for contributors and agents.DESIGN.md — the visual identity, design tokens, and the
full component inventory (generated from src/theme.css + component source;
never hand-edited).docs/patterns/ — examples of existing page compositions
to adapt or replace when a different design serves the request.It's standard shadcn/ui on Base UI: the primitives
under src/components/ui/ are token-pure and the theme lives in the
shadcn-CLI-owned src/theme.css. So you can restyle the whole board with any
shadcn theme — pick one at ui.shadcn.com/create
and apply it, or run pnpm run theme:apply. The full theming workflow is in
docs/theming.md and DESIGN.md.
The template targets Cloudflare Workers (config in
wrangler.jsonc).
pnpm run deploy # builds, then `wrangler deploy`
Going live means pointing the frontend at your own board: a human operator
sets CAVUNO_BOARD (and CAVUNO_API_URL) in wrangler.jsonc before
deploying. That deploy-time grounding is the operator's job, not a code edit
— which is why AGENTS.md tells coding agents to never touch
these values.
Every uncached page is server-rendered, which takes roughly 10–30 ms of CPU
per request, more on a freshly started Worker. Cached pages cost almost
nothing, but crawlers mostly request pages that are not in the cache. The
Workers Free plan allows
10 ms of CPU per request,
so on it many rendered pages fail with Cloudflare's error 1102 ("Worker
exceeded resource limits"), shown as Exceeded CPU Time Limits under the
Worker's Metrics. The
Workers Paid plan
($5/month) raises the limit to 30 seconds per request by default and includes
30 million CPU milliseconds a month, enough for well over a million uncached
pages.
CI runs the complete release checks. During development, use the checks relevant to the change; run the full sequence when validating a release locally:
pnpm run typecheck && pnpm test && pnpm run build
The release checks, including formatting and the Board
API conformance probe (cavuno-board doctor), is documented in
CONTRIBUTING.md and docs/testing.md.
Core Web Vitals are held to budgets, not vibes. One command builds the production bundle, serves it the way a CDN would (Brotli, warmed anonymous HTML cache), and runs Lighthouse's default mobile lab — simulated Slow 4G, 4× CPU throttle — across ten routes (home, listings, and a discovered job / company / salary / blog-post detail page):
pnpm run perf:lighthouse:check
The check fails unless every route meets, on every run:
| Budget | Target |
|---|---|
| Performance score | ≥ 90 worst run, ≥ 95 median |
| Largest Contentful Paint | ≤ 2.5 s |
| Total Blocking Time | ≤ 150 ms |
| Cumulative Layout Shift | ≤ 0.05 |
In practice the routes land well inside these: LCP around two seconds in the
mobile lab, with zero main-thread blocking and near-zero layout shift —
<head> and JSON-LD are composed server-side, so the LCP content arrives in
the HTML rather than behind a client fetch. Run it on your own fork; the
numbers are only worth what you can reproduce.
AGENTS.md contract, and PR expectations./auth/verify-work-email| Candidate profile, applications, saved jobs, and settings | /account, /matches, /saved-jobs, /account/access, /me/applications, /settings |
| Candidate alerts and messaging | /me/alerts, /alerts/confirm, /alerts/manage, /messages, /messages/:conversationId |
| Employer entry and onboarding | /employers, /employers/dashboard, /employers/onboarding/:slug |
| Employer company workspace (jobs + stats chart, applicant pipeline kanban, company profile) | /employers/companies/:slug, /employers/companies/:slug/jobs/new, /employers/companies/:slug/jobs/:jobId/edit, /employers/companies/:slug/jobs/:jobId/applicants, /employers/companies/:slug/profile |
| Post a job (anonymous funnel) | /post |
| Board access and embeds | /password, /embed/jobs |
| Content and legal pages | /about, /privacy-policy, /terms-of-service, /cookie-policy, /impressum |
| Feeds and discovery | /jobs/rss.xml, /blog/rss.xml, /sitemap.xml, /sitemap/:file, /robots.txt, /indexnow-key.txt |
| OpenGraph assets | /companies/:companySlug/jobs/:jobSlug/og, /blog/:postSlug/og, /blog/og/:postSlug.json |
| Platform and integration endpoints | /.well-known/cavuno.json, /site.webmanifest, /ads.txt, /go/* |