From db2e64f11cc6bf7a48943cbddb4ff42fec2d1d67 Mon Sep 17 00:00:00 2001 From: Brijesh Bittu <717550+brijeshb42@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:47:40 +0530 Subject: [PATCH 01/13] [website] Sync careers from the shared API --- ...m-members.yml => sync-website-content.yml} | 47 +++-- docs/data/careers/roles.json | 18 ++ docs/next.config.ts | 4 + docs/pages/careers.tsx | 197 +++--------------- docs/pages/careers/404.tsx | 39 ++++ docs/pages/careers/roles/[slug].tsx | 53 +++++ docs/public/_redirects | 33 +++ docs/scripts/syncCareers.ts | 82 ++++++++ docs/src/components/careers/RoleEntry.tsx | 16 +- .../modules/components/TopLayoutCareers.js | 10 +- package.json | 1 + 11 files changed, 306 insertions(+), 194 deletions(-) rename .github/workflows/{sync-team-members.yml => sync-website-content.yml} (58%) create mode 100644 docs/data/careers/roles.json create mode 100644 docs/pages/careers/404.tsx create mode 100644 docs/pages/careers/roles/[slug].tsx create mode 100644 docs/scripts/syncCareers.ts diff --git a/.github/workflows/sync-team-members.yml b/.github/workflows/sync-website-content.yml similarity index 58% rename from .github/workflows/sync-team-members.yml rename to .github/workflows/sync-website-content.yml index 8130a0968d7db8..c06ead5fa5becb 100644 --- a/.github/workflows/sync-team-members.yml +++ b/.github/workflows/sync-website-content.yml @@ -1,4 +1,4 @@ -name: Sync team members +name: Sync website content on: workflow_dispatch: @@ -13,6 +13,10 @@ on: permissions: {} +concurrency: + group: sync-website-content + cancel-in-progress: false + jobs: sync: if: github.repository == 'mui/material-ui' @@ -22,6 +26,8 @@ jobs: pull-requests: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: master - name: Set up pnpm uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - name: Use Node.js @@ -29,51 +35,60 @@ jobs: with: node-version: '22.23.4' cache: 'pnpm' # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-dependencies - - run: pnpm install + - run: pnpm install --frozen-lockfile - name: Sync team members - id: sync + id: team_members run: | { echo 'summary<> "$GITHUB_OUTPUT" - pnpm prettier + - name: Sync careers + run: pnpm docs:sync-careers + - name: Format synced content + run: pnpm prettier - name: Create or update pull request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SYNC_SUMMARY: ${{ steps.sync.outputs.summary }} + SYNC_SUMMARY: ${{ steps.team_members.outputs.summary }} # Empty on scheduled runs, so fall back to the default reviewers. REVIEWERS: ${{ inputs.reviewers || 'admins' }} run: | set -euo pipefail - if [ -z "$(git status --porcelain)" ]; then - echo 'No team member changes. Skipping PR.' + git add --all -- docs/data/about/teamMembers.json docs/public/static/branding/about docs/data/careers + if git diff --cached --quiet; then + echo 'No website content changes. Skipping PR.' exit 0 fi # `cherry-pick-*` is exempt from branch creation restrictions and required # status checks. `docs-*` is only exempt from the former. - BRANCH=cherry-pick-sync-team-members + BRANCH=cherry-pick-sync-website-content git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git switch -C "$BRANCH" - git commit --all --message '[website] Sync about page team members' - git push --force origin "$BRANCH" + git commit --message '[website] Sync team members and careers' + REMOTE_SHA="$(git ls-remote origin "refs/heads/$BRANCH" | cut -f1)" + git push --force-with-lease="refs/heads/$BRANCH:$REMOTE_SHA" origin "$BRANCH" - BODY="$(printf 'Automated sync of the About page team members from the [mui-about API](https://frontend-public.mui.com/api/mui-about).\n\n%s' "$SYNC_SUMMARY")" + cat > "$RUNNER_TEMP/website-content-pr-body.md" <<'BODY' + Automated sync of the About page team members from the [mui-about API](https://frontend-public.mui.com/api/mui-about), and the careers listing and full role pages from the [careers API](https://frontend-public.mui.com/api/mui-careers). + BODY + printf '\n%s\n' "$SYNC_SUMMARY" >> "$RUNNER_TEMP/website-content-pr-body.md" - PR_NUMBER="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')" + PR_NUMBER="$(gh pr list --base master --head "$BRANCH" --state open --json number --jq '.[0].number')" if [ -n "$PR_NUMBER" ]; then - gh pr edit "$PR_NUMBER" --body "$BODY" + gh pr edit "$PR_NUMBER" --body-file "$RUNNER_TEMP/website-content-pr-body.md" else gh pr create \ + --draft \ --base master \ --head "$BRANCH" \ - --title '[website] Sync about page team members' \ - --body "$BODY" \ + --title '[website] Sync team members and careers' \ + --body-file "$RUNNER_TEMP/website-content-pr-body.md" \ --label website - PR_NUMBER="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')" + PR_NUMBER="$(gh pr list --base master --head "$BRANCH" --state open --json number --jq '.[0].number')" fi REVIEWS_URL="repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/requested_reviewers" diff --git a/docs/data/careers/roles.json b/docs/data/careers/roles.json new file mode 100644 index 00000000000000..7238515a5f62ef --- /dev/null +++ b/docs/data/careers/roles.json @@ -0,0 +1,18 @@ +[ + { + "id": "1e96c9ba-ca98-41da-93ea-77459c509699", + "title": "Director of Business Operations", + "category": "Operations", + "description": "

Details of the role

The company

MUI's story began in 2014 with Material UI, the most successful React implementation of Google's Material Design. This open-source project has paved the way for the fully-fledged startup known as MUI (founded in 2019), which now boasts an ever-expanding ecosystem of React UI products. We're a company of 40 people as of 2026, and we're growing steadily.

The products

MUI is best known for our flagship product, Material UI—but this is just one of the UI libraries we maintain. Material UI is complemented by advanced components through MUI X (Data Grid, Charts, Date and Time Pickers, Tree View) for more complex user interactions and data visualization needs. Base UI is our headless component library. We also host Design Kits and pre-built Templates.

The culture

MUI is a fully remote company with a team that spans the globe. The majority of our work is asynchronous, and we rely on written communication to collaborate. We're radically transparent: a lot of our work happens in public. Each contributor has the freedom to decide how and when they work, and that work is primarily self-directed: it's your responsibility to define and complete your own tasks in a timely manner.

For additional details about the culture, you can check our careers and about pages and also our public Handbook.

Why we're hiring

As MUI scales past 40 people, our biggest bottleneck is increasingly execution in business operations. Without a dedicated business operations leader, executive bandwidth gets tied up in day-to-day tasks rather than high-leverage decisions, critical cross-functional projects stall between departments, etc.

We are hiring a Director of Business Operations to offload executive operations from the CEO and build the machinery required for our products to reach their full potential. In this role, you will take on critical initiatives, ensure that cross-functional friction is resolved, and enforce a high bar of execution across the entire business.

Overall, both our open-source community and our premium products are growing fast. We need talented people to keep up with the demands of our exigent community!

Why this is interesting

You will operate at the intersection of business, revenue, system design (for example, AI), strategy, and organization design. You won't manage processes—you will lead a team that builds scalable foundations, untangles complex cross-functional friction (across Finance, Marketing, Product, Sales, People, Finance, and Engineering), and incubates new capabilities (like RevOps or internal tooling migrations) from 0 to 1.

If you love first-principles problem-solving, data, obsessing over operational details, and building living systems that save organization-wide time at scale, this role offers large autonomy and high strategic leverage. We care deeply about precision, excellence, and deep focus on the inputs (not the outputs): \"How you do anything is how you do everything.\"

Our products empower React developers to build awesome applications faster – we see millions of developers on MUI's docs every year, one million a month.

The role

Key responsibilities

Depending on the day, you'll:

Overall, expect the role to be 60% execution, 40% team management.

Who we're looking for

Required

Nice to have (but not required)

Benefits and compensation

We offer competitive compensation commensurate with your experience level and location and are prepared to pay top market rates for someone who can clearly exceed the role's expectations. You can find the other perks and benefits on the careers page.

Don't meet every requirement? Apply anyway! Research shows that certain folks are less likely to apply for a role than others unless they meet 100% of the outlined qualifications. If this role excites you, we want to hear from you. We'd love for you to share the unique skills, passion, and experience you could bring to MUI.

", + "summary": "", + "applicationUrl": "https://jobs.ashbyhq.com/MUI/1e96c9ba-ca98-41da-93ea-77459c509699/application" + }, + { + "id": "4bfd5851-6252-489d-a7b1-06a704c285a7", + "title": "Operations Engineer", + "category": "Operations", + "description": "

Details of the role

The company

MUI's story began in 2014 with Material UI, the most successful React implementation of Google's Material Design. This open-source project has paved the way for the fully-fledged startup known as MUI (founded in 2019), which now boasts an ever-expanding ecosystem of React UI products. We're a company of 40 people as of 2026, and we're growing steadily.

The products

MUI is best known for our flagship product, Material UI—but this is just one of the UI libraries we maintain. Material UI is complemented by advanced components through MUI X (Data Grid, Charts, Date and Time Pickers, Tree View) for more complex user interactions and data visualization needs. Base UI is our headless component library. We also host Design Kits and pre-built Templates.

The culture

MUI is a fully remote company with a team that spans the globe. The majority of our work is asynchronous, and we rely on written communication to collaborate. We're radically transparent: a lot of our work happens in public. Each contributor has the freedom to decide how and when they work, and that work is primarily self-directed: it's your responsibility to define and complete your own tasks in a timely manner.

For additional details about the culture, you can check our careers and about pages and also our public Handbook.

Why we're hiring

MUI is scaling, but traditional business operations have become a strong bottleneck to our growth. The leadership team is spending too much time on manual, repetitive tasks, building spreadsheets, debugging ETL regressions, wiring SaaS tools together, firefighting operational issues, etc.

Rather than hiring a conventional IT team, we treat internal company operations as an engineering problem. We believe that applying systems thinking, modern software engineering, and AI automation directly to business operations will create immense leverage.

We are opening this role to act as a hands-on force multiplier across the organization—building zero-defect data models, automated internal tools, and AI-driven workflows across Finance, GTM (RevOps, Sales, Marketing), People, and Product.

Overall, both our open-source community and our premium products are growing fast. We need talented people to keep up with the demands of our exigent community!

Why this is interesting

Our products empower React developers to build awesome applications faster – we see millions of developers on MUI's docs every year, one million a month.

The role

Key responsibilities

Depending on the day, you'll:

Who we're looking for

Required

Nice to have (but not required)

Benefits and compensation

We offer competitive compensation commensurate with your experience level and location and are prepared to pay top market rates for someone who can clearly exceed the role's expectations. You can find the other perks and benefits on the careers page.

Don't meet every requirement? Apply anyway! Research shows that certain folks are less likely to apply for a role than others unless they meet 100% of the outlined qualifications. If this role excites you, we want to hear from you. We'd love for you to share the unique skills, passion, and experience you could bring to MUI.

 
", + "summary": "", + "applicationUrl": "https://jobs.ashbyhq.com/MUI/4bfd5851-6252-489d-a7b1-06a704c285a7/application" + } +] diff --git a/docs/next.config.ts b/docs/next.config.ts index 59975b04d67ae7..b81076fb48f2ec 100644 --- a/docs/next.config.ts +++ b/docs/next.config.ts @@ -343,6 +343,10 @@ export default withDeploymentConfig({ ) { return; } + // getStaticPaths provides the concrete URLs for this dynamic page. + if (page.pathname === '/careers/roles/[slug]') { + return; + } if (!page.children) { // map api-docs to api // i: /api-docs/* > /api/* (old structure) diff --git a/docs/pages/careers.tsx b/docs/pages/careers.tsx index 2d648180c8cb05..ad9ec901f94663 100644 --- a/docs/pages/careers.tsx +++ b/docs/pages/careers.tsx @@ -9,6 +9,7 @@ import OurValues from 'docs/src/components/about/OurValues'; import PerksBenefits from 'docs/src/components/careers/PerksBenefits'; import CareersFaq from 'docs/src/components/careers/CareersFaq'; import RoleEntry from 'docs/src/components/careers/RoleEntry'; +import rolesData from 'docs/data/careers/roles.json'; import AppHeader from 'docs/src/layouts/AppHeader'; import AppFooter from 'docs/src/layouts/AppFooter'; import GradientText from 'docs/src/components/typography/GradientText'; @@ -18,128 +19,26 @@ import SectionHeadline from '@mui/internal-core-docs/SectionHeadline'; import { AppHeaderBanner, AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; -type Role = { +interface CareerRole { + id: string; title: string; - description: string; - url: string; -}; - -type RoleCategory = { - title: string; - roles: Role[]; -}; - -const openRolesData: RoleCategory[] = [ - { - title: 'Engineering', - roles: [ - // { - // title: 'React Engineer — Material UI Maintainer', - // description: - // 'You will provide guidance to the community and solve their struggle, working on one of the most popular React UI library.', - // url: '/careers/react-engineer-material-ui-maintainer/', - // }, - // { - // title: 'Product Engineer — Store', - // description: - // 'You will lead the technical, product, and operational development of the store.', - // url: '/careers/product-engineer-store/', - // }, - // { - // title: 'React Engineer — xCharts', - // description: - // 'You will help form the xCharts team, build ambitious and complex new features, work on strategic problems, and help grow adoption.', - // url: '/careers/react-engineer-x-charts/', - // }, - // { - // title: 'React Engineer — eXplore', - // description: - // 'You will help eXplore, the team behind the Pickers and Tree View components, build the most comprehensive UI library the world has ever seen.', - // url: '/careers/react-engineer-explore/', - // }, - // { - // title: 'React Engineer — X', - // description: - // 'You will strengthen the MUI X product, build ambitious and complex new features, work on strategic problems, and help grow adoption.', - // url: '/careers/react-engineer-x/', - // }, - ], - }, - { - title: 'Sales', - roles: [ - // { - // title: 'Account Executive', - // description: - // 'You will be a key player in driving revenue growth and building strong customer relationships.', - // url: '/careers/account-executive/', - // }, - // { - // title: 'Account Manager', - // description: - // 'You will be working with our customers to ensure the successful renewal and retention of existing client contracts.', - // url: '/careers/account-manager/', - // }, - ], - }, - { - title: 'People', - roles: [ - // { - // title: 'Technical Recruiter', - // description: 'MUI is looking for an experienced Tech Recruiter to join our People team.', - // url: '/careers/technical-recruiter/', - // }, - ], - }, - { - title: 'Marketing', - roles: [ - // { - // title: 'Product Marketing Manager', - // description: 'Establish MUI product positioning and messaging.', - // url: '/careers/product-marketing-manager/', - // }, - ], - }, - { - title: 'Design', - roles: [], - }, - { - title: 'Developer Experience', - roles: [], - }, -]; - -const nextRolesData = [ - { - title: 'Engineering', - roles: [ - // { - // title: 'React Community Engineer — X', - // description: - // 'You will provide guidance to the community and solve their struggle, working primarily in the advanced components team.', - // url: '/careers/react-community-engineer/', - // }, - ], - }, - { - title: 'Sales', - roles: [], - }, - { - title: 'People', - roles: [], - }, - { - title: 'Marketing', - roles: [], - }, -] as typeof openRolesData; + category: string; + summary: string; +} -const openRolesCount = openRolesData.reduce((acc, item) => acc + item.roles.length, 0); -const nextRolesCount = nextRolesData.reduce((acc, item) => acc + item.roles.length, 0); +// Synced from the careers API with `pnpm docs:sync-careers`. +const roles: CareerRole[] = rolesData; +const rolesByCategory = new Map(); +for (const role of roles) { + const category = rolesByCategory.get(role.category) ?? []; + category.push(role); + rolesByCategory.set(role.category, category); +} +const openRolesData = Array.from(rolesByCategory, ([title, categoryRoles]) => ({ + title, + roles: categoryRoles, +})); +const openRolesCount = roles.length; export default function Careers() { return ( @@ -203,10 +102,10 @@ export default function Careers() { {category.roles.map((role) => ( ))} @@ -227,53 +126,15 @@ export default function Careers() { } description={ - nextRolesCount > 0 ? ( - - We're not actively hiring for these roles yet, but you're welcome to - apply for future consideration. If none of these roles match your profile, you - can apply to{' '} - - the dream job - {' '} - and tell us more about what you bring to the table. - - ) : ( - - You're welcome to apply for future consideration. You can apply to{' '} - - the dream job - {' '} - and tell us more about what you bring to the table. - - ) + + You're welcome to apply for future consideration. You can apply to{' '} + + the dream job + {' '} + and tell us more about what you bring to the table. + } /> - {nextRolesCount > 0 ? ( - - - }> - {nextRolesData - .filter((category) => category.roles.length > 0) - .map((category) => { - return ( - - - {category.title} - - {category.roles.map((role) => ( - - ))} - - ); - })} - - - ) : null} diff --git a/docs/pages/careers/404.tsx b/docs/pages/careers/404.tsx new file mode 100644 index 00000000000000..b72de3460c0910 --- /dev/null +++ b/docs/pages/careers/404.tsx @@ -0,0 +1,39 @@ +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import { BrandingCssVarsProvider } from '@mui/internal-core-docs/branding'; +import { AppHeaderBanner, AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; +import SectionHeadline from '@mui/internal-core-docs/SectionHeadline'; +import AppHeader from 'docs/src/layouts/AppHeader'; +import AppFooter from 'docs/src/layouts/AppFooter'; +import Section from 'docs/src/layouts/Section'; + +export default function CareerNotFound() { + return ( + + + + + + +
+
+ + This job doesn't exist + + } + description="This role is no longer available, or the link may be incorrect. Explore our current openings to find another opportunity." + /> + +
+ +
+ +
+ ); +} diff --git a/docs/pages/careers/roles/[slug].tsx b/docs/pages/careers/roles/[slug].tsx new file mode 100644 index 00000000000000..5a18ef3b60ce57 --- /dev/null +++ b/docs/pages/careers/roles/[slug].tsx @@ -0,0 +1,53 @@ +import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'; +import { MarkdownElement } from '@mui/internal-core-docs/MarkdownDocs'; +import rolesData from 'docs/data/careers/roles.json'; +import TopLayoutCareers from 'docs/src/modules/components/TopLayoutCareers'; + +interface CareerJob { + id: string; + title: string; + description: string; + summary: string; + applicationUrl: string; +} + +const jobs: CareerJob[] = rolesData; + +export function getStaticPaths() { + return { + paths: jobs.map((job) => ({ params: { slug: job.id } })), + fallback: false, + }; +} + +export function getStaticProps({ params }: GetStaticPropsContext<{ slug: string }>) { + const job = jobs.find((posting) => posting.id === params?.slug); + if (!job) { + return { notFound: true as const }; + } + + return { props: { job } }; +} + +export default function CareerRole({ job }: InferGetStaticPropsType) { + const docs = { + en: { + title: job.title, + description: job.summary || 'Explore this career opportunity and apply to join the MUI team.', + rendered: [], + }, + }; + + return ( + + +

{job.title}

+ {/* eslint-disable-next-line react/no-danger -- Render the HTML description supplied by the careers API. */} +
+

+ Apply now for this position 📮 +

+ + + ); +} diff --git a/docs/public/_redirects b/docs/public/_redirects index 4fadd655cffb7d..a2335f2ba4415a 100644 --- a/docs/public/_redirects +++ b/docs/public/_redirects @@ -27,6 +27,39 @@ # The Discord server is closed, point the stale links to the support page. /r/discord /material-ui/getting-started/support/ 302 +# Careers pages migrated to Ashby job IDs. +# Force redirects because the handwritten pages still exist. +/careers/accessibility-engineer/ /careers/roles/ebc6db2a-fda9-431e-b45a-692377ef74aa/ 301! +/careers/account-executive/ /careers/roles/a0078669-159a-416e-9412-e0ea65e1664d/ 301! +/careers/account-manager/ /careers/roles/04091577-abf8-4654-bc15-929755de0170/ 301! +/careers/code-infra-engineer/ /careers/roles/a2e74037-a44a-471b-9ac5-c1a0408034e7/ 301! +/careers/design-engineer-x-grid/ /careers/roles/5a1c18de-e0b5-4253-aca1-e3f261112920/ 301! +/careers/design-engineer/ /careers/roles/c45a0b2d-aa37-4dec-8bcd-e70040f3af49/ 301! +/careers/developer-advocate/ /careers/roles/28287eeb-88d2-465f-96d7-e7fd99fabd7d/ 301! +/careers/full-stack-engineer/ /careers/roles/4f76b45b-a18e-4bdb-8a34-8bb5a3c9c2ef/ 301! +/careers/head-of-operations/ /careers/roles/86ab11dc-0320-4e14-9b5b-7c8b3fb5bb71/ 301! +/careers/people-operation-manager/ /careers/roles/b46968b7-dde3-4b91-9785-a05aa0a816d8/ 301! +/careers/product-engineer-store/ /careers/roles/e641bac3-5538-4ec8-8d73-eaa1a03704d6/ 301! +/careers/product-marketing-manager/ /careers/roles/7e1d14d3-03e9-46f8-9453-ffe1c7a3b8d8/ 301! +/careers/react-community-engineer/ /careers/roles/e362a14c-6674-4ac9-8eee-bcf44a8767c8/ 301! +/careers/react-engineer-core/ /careers/roles/e6d7cc26-3003-442f-bafb-464373fb2624/ 301! +/careers/react-engineer-docs-infra/ /careers/roles/673f3178-56f4-47fd-bd2b-6dcba57758aa/ 301! +/careers/react-engineer-explore/ /careers/roles/f7b1b8cd-b8b7-4565-92de-34c5f49581c6/ 301! +/careers/react-engineer-material-ui-maintainer/ /careers/roles/f28a1482-22fb-4cf8-9263-092ec0f81657/ 301! +/careers/react-engineer-x-charts/ /careers/roles/2a1b0084-53aa-4aca-84ec-f8cfe25093ae/ 301! +/careers/react-engineer-x-grid/ /careers/roles/77584e4c-9379-45f3-a294-6e50bf250383/ 301! +/careers/react-engineer-x/ /careers/roles/77584e4c-9379-45f3-a294-6e50bf250383/ 301! +/careers/react-tech-lead-core/ /careers/roles/73e07586-d3aa-43b3-bfef-8eec52884e2a/ 301! +/careers/react-tech-lead-x-grid/ /careers/roles/dffc8ead-fab1-4a07-8b44-a932e2838e0e/ 301! +/careers/senior-designer/ /careers/roles/4ea9d324-3350-48d5-bfab-706ebfc23ed2/ 301! +/careers/staff-engineer-pigment-css/ /careers/roles/c1a372c0-7ea0-4edf-b317-5327431b14aa/ 301! +/careers/staff-ui-engineer-base-ui/ /careers/roles/3094f65a-0144-4bd2-ba5b-6b9c61040858/ 301! +/careers/support-agent/ /careers/roles/79a9b5ec-6fb3-41ec-b48b-0a792ced7c19/ 301! +/careers/technical-recruiter/ /careers/roles/0738df5d-6827-45d4-a480-a8b04b7e01bf/ 301! +# Serve the careers-specific 404 for closed or unknown roles. Existing +# generated pages take precedence over this unforced rule. +/careers/roles/* /careers/404/ 404 + # Legacy redirection # Added in chronological order (the last line is the most recent one) # To be removed 3+ years after being introduced diff --git a/docs/scripts/syncCareers.ts b/docs/scripts/syncCareers.ts new file mode 100644 index 00000000000000..2c6187da1e2cd5 --- /dev/null +++ b/docs/scripts/syncCareers.ts @@ -0,0 +1,82 @@ +import * as fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { format } from 'prettier'; + +interface CareerJob { + id: string; + title: string; + category: string; + description: string; + summary?: string | null; + applicationUrl: string; +} + +const defaultDocsDirectory = fileURLToPath(new URL('..', import.meta.url)); + +function isCareerJob(value: unknown): value is CareerJob { + if (!value || typeof value !== 'object') { + return false; + } + const job = value as Record; + return ( + typeof job.id === 'string' && + /^[a-zA-Z0-9-]+$/.test(job.id) && + typeof job.title === 'string' && + job.title.trim().length > 0 && + typeof job.category === 'string' && + job.category.trim().length > 0 && + typeof job.description === 'string' && + (job.summary === undefined || job.summary === null || typeof job.summary === 'string') && + typeof job.applicationUrl === 'string' && + URL.canParse(job.applicationUrl) && + new URL(job.applicationUrl).protocol === 'https:' + ); +} + +export default async function syncCareers( + apiUrl = process.env.MUI_CAREERS_API_URL || 'https://frontend-public.mui.com/api/mui-careers', + docsDirectory = defaultDocsDirectory, +) { + const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30_000) }); + if (!response.ok) { + throw new Error(`Failed to fetch careers: ${response.status} ${response.statusText}`); + } + + const payload: unknown = await response.json(); + if ( + !payload || + typeof payload !== 'object' || + !('data' in payload) || + !Array.isArray(payload.data) || + !payload.data.every(isCareerJob) + ) { + throw new Error('Invalid careers response. Expected a data array of complete job postings.'); + } + const jobs: CareerJob[] = payload.data; + if (new Set(jobs.map((job) => job.id)).size !== jobs.length) { + throw new Error('Invalid careers response. Job IDs must be unique.'); + } + jobs.sort((a, b) => a.category.localeCompare(b.category) || a.title.localeCompare(b.title)); + + const dataDirectory = path.join(docsDirectory, 'data/careers'); + const roles = await format( + JSON.stringify( + jobs.map((job) => ({ + id: job.id, + title: job.title, + category: job.category, + description: job.description, + summary: job.summary?.trim() || '', + applicationUrl: job.applicationUrl, + })), + ), + { parser: 'json' }, + ); + await fs.mkdir(dataDirectory, { recursive: true }); + await fs.writeFile(path.join(dataDirectory, 'roles.json'), roles); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { + syncCareers(); +} diff --git a/docs/src/components/careers/RoleEntry.tsx b/docs/src/components/careers/RoleEntry.tsx index c7b0580e43edd5..32c42876c1c251 100644 --- a/docs/src/components/careers/RoleEntry.tsx +++ b/docs/src/components/careers/RoleEntry.tsx @@ -30,9 +30,11 @@ export default function RoleEntry(props: RoleProps) { > {props.title} - - {props.description} - + {props.description ? ( + + {props.description} + + ) : null}