chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,92 @@
import { Suspense } from 'react'
import type { SearchParams } from 'nuqs/server'
import { getAshbyJobs } from '@/lib/ashby/jobs'
import {
filterPostings,
groupByDepartment,
hasActiveFilters,
JobBoard,
JobGroups,
} from '@/app/(landing)/careers/components/job-board'
import { careersSearchParamsCache } from '@/app/(landing)/careers/search-params'
interface CareersProps {
searchParams: Promise<SearchParams>
}
/**
* The careers page — a mission-led hero above the live open-roles board. Roles
* are pulled from Sim's public Ashby job board at build/revalidate time
* ({@link getAshbyJobs}) and server-rendered in full, so every posting is in the
* crawlable HTML; the interactive {@link JobBoard} hydrates on top to add
* Team/Location filtering.
*
* Both sections share the landing gutter — capped and centered at `max-w-[1460px]`
* with the navbar-aligned `px-20 max-lg:px-8 max-sm:px-5` so the headline starts on
* the same vertical line as the wordmark. The hero carries the single `<h1>`
* (containing "Sim" and "AI workspace") plus an sr-only product summary for AI
* citation (landing CLAUDE.md → GEO); the roles section owns its own `<h2>`.
*
* Because {@link JobBoard} reads the URL via nuqs (`useSearchParams`), it sits under
* a `<Suspense>` boundary. The page parses the same `?team=`/`?location=` query on
* the server ({@link careersSearchParamsCache}) and pre-filters the fallback to
* match, so a deep-linked filter renders the correct roles server-side — the list
* never flashes unfiltered before the client board hydrates.
*/
export default async function Careers({ searchParams }: CareersProps) {
const [{ team, location }, postings] = await Promise.all([
careersSearchParamsCache.parse(searchParams),
getAshbyJobs(),
])
const fallbackGroups = groupByDepartment(filterPostings(postings, team, location))
return (
<main id='main-content'>
<section
id='careers-hero'
aria-labelledby='careers-heading'
className='mx-auto flex w-full max-w-[1460px] flex-col gap-5 px-20 pt-20 pb-10 max-sm:px-5 max-sm:pt-16 max-lg:px-8'
>
<p className='sr-only'>
Careers at Sim, the open-source AI workspace where teams build, deploy, and manage AI
agents. Sim is hiring engineers, designers, and go-to-market builders to help teams
automate real work across 1,000+ integrations and every major LLM visually,
conversationally, or with code.
</p>
<h1
id='careers-heading'
className='max-w-[24ch] text-balance text-[48px] text-[var(--text-primary)] leading-[1.1] max-sm:text-[32px] max-xl:text-[40px]'
>
Help build Sim, the AI workspace for teams.
</h1>
<p className='max-w-[60ch] text-pretty text-[var(--text-body)] text-lg leading-[1.5] max-sm:text-base'>
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. We're
a small, high-agency team shipping fast to thousands of builders. If you want to own real
work and shape the workspace teams live in, we'd love to meet you.
</p>
</section>
<section
id='open-roles'
aria-labelledby='open-roles-heading'
className='mx-auto flex w-full max-w-[1460px] flex-col gap-10 px-20 pt-6 pb-24 max-sm:px-5 max-sm:pb-16 max-lg:px-8'
>
<h2
id='open-roles-heading'
className='text-[24px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'
>
Open roles
</h2>
<Suspense
fallback={
<JobGroups groups={fallbackGroups} filtersActive={hasActiveFilters(team, location)} />
}
>
<JobBoard postings={postings} />
</Suspense>
</section>
</main>
)
}
@@ -0,0 +1,3 @@
export { JobBoard } from './job-board'
export { JobGroups } from './job-groups'
export { filterPostings, groupByDepartment, hasActiveFilters } from './utils'
@@ -0,0 +1,74 @@
'use client'
import { ChipSelect, type ChipSelectOption } from '@sim/emcn'
import { useQueryStates } from 'nuqs'
import type { CareerPosting } from '@/lib/ashby/jobs'
import { JobGroups } from '@/app/(landing)/careers/components/job-board/job-groups'
import {
filterPostings,
groupByDepartment,
hasActiveFilters,
} from '@/app/(landing)/careers/components/job-board/utils'
import {
ALL_FILTER_VALUE,
careersParsers,
careersUrlKeys,
} from '@/app/(landing)/careers/search-params'
interface JobBoardProps {
postings: CareerPosting[]
}
/** Builds `{ label, value }` options for a filter, with an "All" row at the top. */
function toFilterOptions(values: string[], allLabel: string): ChipSelectOption[] {
return [
{ label: allLabel, value: ALL_FILTER_VALUE },
...values.map((value) => ({ label: value, value })),
]
}
/** Distinct, alphabetically sorted values from a list. */
function uniqueSorted(values: string[]): string[] {
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b))
}
/**
* The interactive open-roles board — the single `'use client'` leaf on the
* careers page. Every posting is server-rendered into the HTML (via the static
* {@link JobGroups} Suspense fallback in `careers.tsx`), so all roles stay
* crawlable; this leaf hydrates on top to add Team/Location filtering. Filter
* state lives in the URL via nuqs (`?team=`/`?location=`) so a filtered view is
* shareable and survives reload/back-forward. The filter set is small and
* static, so filtering reads the instant URL value directly (no debounce).
*/
export function JobBoard({ postings }: JobBoardProps) {
const [{ team, location }, setFilters] = useQueryStates(careersParsers, careersUrlKeys)
const teamOptions = toFilterOptions(uniqueSorted(postings.map((p) => p.department)), 'All teams')
const locationOptions = toFilterOptions(
uniqueSorted(postings.flatMap((p) => (p.location ? [p.location] : []))),
'All locations'
)
const groups = groupByDepartment(filterPostings(postings, team, location))
return (
<div className='flex flex-col gap-10'>
<div className='flex flex-wrap items-center gap-3'>
<ChipSelect
options={teamOptions}
value={team}
onChange={(value) => setFilters({ team: value })}
aria-label='Filter roles by team'
/>
<ChipSelect
options={locationOptions}
value={location}
onChange={(value) => setFilters({ location: value })}
aria-label='Filter roles by location'
/>
</div>
<JobGroups groups={groups} filtersActive={hasActiveFilters(team, location)} />
</div>
)
}
@@ -0,0 +1,114 @@
import { cn } from '@sim/emcn'
import { ArrowRight } from '@sim/emcn/icons'
import type { CareerPosting } from '@/lib/ashby/jobs'
import type { DepartmentGroup } from '@/app/(landing)/careers/components/job-board/utils'
/** Empty-state copy: distinguishes a truly empty board from a filtered-to-zero view. */
const NO_OPEN_ROLES_MESSAGE = 'No open roles right now — check back soon.'
const NO_MATCHING_ROLES_MESSAGE =
'No roles match these filters right now. Try clearing them, or check back soon.'
interface JobGroupsProps {
groups: DepartmentGroup[]
/**
* Whether a Team/Location filter is active. Selects the empty-state copy so an
* unfiltered empty board ("no open roles") never reads as a filtered miss ("no
* matches") — and the server fallback and client board always agree.
*/
filtersActive?: boolean
}
/**
* The presentational open-roles list: one labeled section per department, each a
* list of {@link JobRow}s. Server-safe (no client hooks) so it renders both as
* the static Suspense fallback and inside the client {@link JobBoard}.
*/
export function JobGroups({ groups, filtersActive = false }: JobGroupsProps) {
if (groups.length === 0) {
return (
<p className='py-10 text-[var(--text-muted)] text-base'>
{filtersActive ? NO_MATCHING_ROLES_MESSAGE : NO_OPEN_ROLES_MESSAGE}
</p>
)
}
return (
<div className='flex flex-col gap-12'>
{groups.map((group) => (
<section
key={group.department}
aria-label={`${group.department} roles`}
className='flex flex-col'
>
<h3 className='pb-2 font-medium text-[var(--text-muted)] text-sm'>{group.department}</h3>
<ul className='flex flex-col'>
{group.postings.map((posting) => (
<li key={posting.id}>
<JobRow posting={posting} />
</li>
))}
</ul>
</section>
))}
</div>
)
}
interface JobRowProps {
posting: CareerPosting
}
/**
* A single role row: title over a metadata line, with an "Apply" affordance that
* links out to the posting on Ashby. The whole row is the link target; hovering
* tints the row and advances the arrow. The metadata values are de-duplicated
* because a remote posting normalizes both `location` and `workplaceType` to
* "Remote", which would otherwise render "Remote · Remote" and collide as keys.
*/
function JobRow({ posting }: JobRowProps) {
const meta = Array.from(
new Set(
[
posting.location,
posting.employmentType,
posting.workplaceType,
posting.compensationSummary,
].filter((value): value is string => Boolean(value))
)
)
return (
<a
href={posting.jobUrl}
target='_blank'
rel='noopener noreferrer'
className={cn(
'-mx-3 group flex items-center justify-between gap-6 rounded-lg border-[var(--border)]',
'border-t px-3 py-5 transition-colors hover:bg-[var(--surface-hover)]'
)}
>
<div className='flex min-w-0 flex-col gap-1.5'>
<h4 className='truncate font-medium text-[var(--text-primary)] text-base'>
{posting.title}
</h4>
<div className='flex flex-wrap items-center gap-x-2 gap-y-1 text-[var(--text-muted)] text-sm'>
{meta.map((item, index) => (
<span key={item} className='flex items-center gap-2'>
{index > 0 && (
<span aria-hidden className='text-[var(--text-muted)]'>
·
</span>
)}
{item}
</span>
))}
</div>
</div>
<span className='flex shrink-0 items-center gap-1.5 font-medium text-[var(--text-body)] text-sm'>
Apply
<ArrowRight className='size-[14px] text-[var(--text-icon)] transition-transform group-hover:translate-x-0.5' />
</span>
</a>
)
}
@@ -0,0 +1,44 @@
import type { CareerPosting } from '@/lib/ashby/jobs'
import { ALL_FILTER_VALUE } from '@/app/(landing)/careers/search-params'
export interface DepartmentGroup {
department: string
postings: CareerPosting[]
}
/**
* Narrows postings to a selected Team and Location, treating {@link ALL_FILTER_VALUE}
* as "any". Shared by the server-rendered fallback and the client board so a
* deep-linked filter resolves to the exact same set on both sides.
*/
export function filterPostings(
postings: CareerPosting[],
team: string,
location: string
): CareerPosting[] {
return postings.filter(
(posting) =>
(team === ALL_FILTER_VALUE || posting.department === team) &&
(location === ALL_FILTER_VALUE || posting.location === location)
)
}
/** Whether either the Team or Location filter is narrowing the board. */
export function hasActiveFilters(team: string, location: string): boolean {
return team !== ALL_FILTER_VALUE || location !== ALL_FILTER_VALUE
}
/**
* Buckets postings by department, preserving their incoming order (the fetcher
* pre-sorts by department then title). Shared by the interactive board and its
* static Suspense fallback so the two can never render a different grouping.
*/
export function groupByDepartment(postings: CareerPosting[]): DepartmentGroup[] {
const byDepartment = new Map<string, CareerPosting[]>()
for (const posting of postings) {
const bucket = byDepartment.get(posting.department)
if (bucket) bucket.push(posting)
else byDepartment.set(posting.department, [posting])
}
return Array.from(byDepartment, ([department, items]) => ({ department, postings: items }))
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { buildLandingMetadata, withFilteredNoindex } from '@/lib/landing/seo'
import Careers from '@/app/(landing)/careers/careers'
import { ALL_FILTER_VALUE, careersSearchParamsCache } from '@/app/(landing)/careers/search-params'
/**
* `team`/`location` render a genuinely different server-rendered job list (see
* search-params.ts), so filtered URLs are noindexed rather than
* self-canonicalized — same policy as the integrations/models/blog catalogs.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
const { team, location } = await careersSearchParamsCache.parse(searchParams)
const isFiltered = team !== ALL_FILTER_VALUE || location !== ALL_FILTER_VALUE
const base = buildLandingMetadata({
title: 'Careers | Sim, the AI Workspace',
description:
'Join Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. See open engineering, design, and go-to-market roles.',
path: '/careers',
keywords:
'Sim careers, Sim jobs, AI workspace jobs, AI agent engineering jobs, open source jobs',
})
return withFilteredNoindex(base, isFiltered)
}
export default function Page({ searchParams }: { searchParams: Promise<SearchParams> }) {
return <Careers searchParams={searchParams} />
}
@@ -0,0 +1,35 @@
import { createSearchParamsCache, parseAsString } from 'nuqs/server'
/**
* Sentinel value for an inactive filter — matches every posting. Namespaced with
* underscores so it can never collide with a real Ashby department or location
* value (e.g. a team literally named "all").
*/
export const ALL_FILTER_VALUE = '__all__'
/**
* Co-located, typed URL query params for the careers job board's Team and
* Location filters. Shareable, deep-linkable view-state over an already-rendered
* list, so it lives in the URL (nuqs) — never in a store. The values are dynamic
* (departments/locations come from the live board), so plain string parsers with
* an `all` sentinel default rather than a fixed literal set.
*/
export const careersParsers = {
team: parseAsString.withDefault(ALL_FILTER_VALUE),
location: parseAsString.withDefault(ALL_FILTER_VALUE),
} as const
/** Clean URLs, no back-stack churn — the filters are a passive view switch. */
export const careersUrlKeys = {
history: 'replace',
shallow: true,
clearOnDefault: true,
} as const
/**
* Server-side reader for the same parser map. The page parses the request's
* query with this so the statically-rendered fallback is filtered to match a
* deep-linked `?team=`/`?location=` URL — the roles never flash unfiltered before
* the client board hydrates.
*/
export const careersSearchParamsCache = createSearchParamsCache(careersParsers)