Skip to content

Job Board Plugin #58

@olliethedev

Description

@olliethedev

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 status field
  • Closing date field (auto-archives when past)

SEO & SSG

  • prefetchForRoute support for "list", "detail", and "department" route keys
  • generateStaticParams for job detail pages from getAllJobs
  • 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 const

Plugin 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 applyUrl to link to a Form Builder form)
  • 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_TYPE in cmsBackendPlugin, adding jobBoardClientPlugin in the client stack
  • Schema referenceAutoTypeTable for jobSchema and JobBoardClientConfig
  • Routes — table of route keys, paths, and what each page renders
  • HooksuseJobs, useJob with filter examples
  • OverridesAutoTypeTable for JobBoardPluginOverrides
  • SSGprefetchForRoute route key table + Next.js page.tsx example (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 — prefetchForRoute infrastructure already exists

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions