-
Notifications
You must be signed in to change notification settings - Fork 18
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Overview
Add a job board plugin that provides a full-featured careers/jobs section. Like the UI builder, it sits on top of the CMS plugin — it registers a pre-configured ContentTypeConfig and provides its own typed pages and hooks that understand the job-board domain.
This means:
- No new database schema — jobs are just CMS content items
- The CMS admin UI works out of the box for job management
- The job board provides polished public-facing pages (listing, detail, apply)
Core Features
Job Listings
- Job listings page with filters (department, location, type)
- Individual job detail page
- Application form (integrates with the Form Builder plugin)
- Department / location / employment type taxonomies
Admin (via CMS plugin)
- Create/edit/publish/archive jobs through standard CMS UI
- Draft → Published → Archived lifecycle with
statusfield - Closing date field (auto-archives when past)
SEO & SSG
-
prefetchForRoutesupport for"list","detail", and"department"route keys -
generateStaticParamsfor job detail pages fromgetAllJobs - Structured data (
schema.org/JobPosting) in meta tags
Schema (CMS content type)
import { z } from "zod"
import type { ContentTypeConfig } from "@btst/stack/plugins/cms/api"
export const jobSchema = z.object({
title: z.string(),
department: z.string().meta({ fieldType: "select" }),
location: z.string(),
employmentType: z.enum(["full-time", "part-time", "contract", "internship"]).meta({ fieldType: "select" }),
remote: z.boolean().default(false),
description: z.string().meta({ fieldType: "markdown" }),
requirements: z.string().meta({ fieldType: "markdown" }),
salary: z.string().optional(),
closingDate: z.string().optional().meta({ fieldType: "date" }),
applicationFormId: z.string().optional(), // links to a Form Builder form
status: z.enum(["draft", "published", "archived"]).default("draft").meta({ fieldType: "select" }),
})
export const JOB_BOARD_CONTENT_TYPE: ContentTypeConfig = {
name: "Job Posting",
slug: "job-posting",
description: "Careers / job board postings",
schema: jobSchema,
}
export const JOB_BOARD_TYPE_SLUG = "job-posting" as constPlugin Structure
Following the UI builder pattern:
src/plugins/job-board/
├── schemas.ts # jobSchema, JOB_BOARD_CONTENT_TYPE
├── types.ts # JobBoardClientHooks, overrides, etc.
├── index.ts # re-export JOB_BOARD_CONTENT_TYPE
├── client.css # component styles
├── style.css # Tailwind source directives
└── client/
├── plugin.tsx # defineClientPlugin — routes, loaders, meta
├── overrides.ts # JobBoardPluginOverrides type
├── localization/ # i18n strings
├── hooks/
│ ├── job-board-hooks.tsx # useJobs, useJob
│ └── index.tsx
└── components/
├── pages/
│ ├── job-list-page.tsx # /jobs
│ ├── job-list-page.internal.tsx
│ ├── job-detail-page.tsx # /jobs/:slug
│ ├── job-detail-page.internal.tsx
│ └── department-page.tsx # /jobs/department/:dept
├── shared/
│ ├── job-card.tsx
│ ├── job-filters.tsx
│ ├── apply-button.tsx # links out to Form Builder form
│ ├── empty-state.tsx
│ ├── default-error.tsx
│ └── page-wrapper.tsx
└── loading/
├── job-list-skeleton.tsx
└── job-detail-skeleton.tsx
Routes
| Route | Path | Description |
|---|---|---|
list |
/jobs |
Paginated job listing with filters |
department |
/jobs/department/:dept |
Jobs filtered by department |
detail |
/jobs/:slug |
Full job detail + apply CTA |
Consumer Setup
// lib/stack.ts
import { cmsBackendPlugin } from "@btst/stack/plugins/cms/api"
import { JOB_BOARD_CONTENT_TYPE } from "@btst/stack/plugins/job-board"
cms: cmsBackendPlugin({
contentTypes: [JOB_BOARD_CONTENT_TYPE],
})// lib/stack-client.tsx
import { jobBoardClientPlugin } from "@btst/stack/plugins/job-board/client"
plugins: [
jobBoardClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBaseURL: "https://example.com",
siteBasePath: "",
queryClient,
overrides: {
navigate: router.push,
Link,
},
}),
]Hooks
import { useJobs, useJob } from "@btst/stack/plugins/job-board/client/hooks"
// Paginated job list with optional filters
const { data, fetchNextPage } = useJobs({ department: "engineering", remote: true })
// Single job by slug
const { data: job } = useJob("senior-frontend-engineer")SSG Support
// app/pages/jobs/page.tsx
export async function generateStaticParams() { return [{}] }
export default async function JobsPage() {
const queryClient = getOrCreateQueryClient()
await myStack.api.cms.prefetchForRoute("contentList", queryClient, { typeSlug: "job-posting" })
// ...
}prefetchForRoute re-uses the CMS plugin's existing SSG infrastructure — no separate implementation needed.
Non-Goals (v1)
- Applicant tracking / ATS
- Authentication-gated applications (can just use
applyUrlto link to aForm Builderform) - Email notifications (use lifecycle hooks)
- Multi-company / multi-tenant
Plugin Configuration Options
| Option | Type | Description |
|---|---|---|
apiBaseURL |
string |
Base URL for API calls |
apiBasePath |
string |
API route prefix |
siteBaseURL |
string |
Base URL for meta/OG tags |
siteBasePath |
string |
Mount path (e.g. "" or "/careers") |
queryClient |
QueryClient |
Shared React Query client |
overrides |
JobBoardPluginOverrides |
Component/navigation overrides |
hooks |
JobBoardClientHooks |
Lifecycle hooks |
Documentation
Add docs/content/docs/plugins/job-board.mdx covering:
- Overview — what the plugin is, that it sits on top of the CMS plugin
- Setup — registering
JOB_BOARD_CONTENT_TYPEincmsBackendPlugin, addingjobBoardClientPluginin the client stack - Schema reference —
AutoTypeTableforjobSchemaandJobBoardClientConfig - Routes — table of route keys, paths, and what each page renders
- Hooks —
useJobs,useJobwith filter examples - Overrides —
AutoTypeTableforJobBoardPluginOverrides - SSG —
prefetchForRouteroute key table + Next.jspage.tsxexample (mirrors the blog/CMS SSG docs pattern) - AI Chat integration — job detail page registers read-only context (
routeName: "job-board-detail", suggestions like "What are the requirements for this role?")
Related Issues / Prior Art
- UI builder plugin — same CMS-backed pattern this would follow
- Calendar Booking Plugin #40 Calendar Booking Plugin — similar domain plugin concept
- CMS plugin SSG docs —
prefetchForRouteinfrastructure already exists
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request