diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 437eb49..48a05b6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,6 +4,11 @@ on: push: branches: - main + schedule: + # Weekday morning US Eastern (13:00 UTC). Rebuilds listings from Paychex + # without a commit. A failed feed fetch aborts before the S3 sync. + - cron: "0 13 * * 1-5" + workflow_dispatch: permissions: id-token: write # Required for OIDC diff --git a/README.md b/README.md index fa9d4a8..3219781 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,8 @@ npm run build # build to _site/ | Social Accountability | `/social-accountability/` | | Privacy Policy | `/privacy-policy/` | | Terms of Service | `/terms-of-service/` | -| Job: Dispatcher | `/careers/dispatcher/` | -| Job: Scheduling Coordinator | `/careers/scheduling-coordinator/` | -| Job: Traveling Maintenance/Electrical Assistant | `/careers/traveling-maintenance-assistant/` | + +Open roles on `/careers/` are rendered at build time from the Paychex portal at `https://jobs.appone.com/seahaven`. Learn more links go to each posting on `apply.appone.com`. The old job URLs redirect to the portal. ## Changelog * 12-JUN-2026: Frontend polish — self-hosted fonts (removed render-blocking Google Fonts), lazy-loaded reCAPTCHA, BreadcrumbList JSON-LD on interior pages + richer LocalBusiness (geo/areaServed/hasMap), removed dead CSS classes and `!important` nav hacks. diff --git a/_data/jobs.js b/_data/jobs.js new file mode 100644 index 0000000..7a6e261 --- /dev/null +++ b/_data/jobs.js @@ -0,0 +1,80 @@ +const FEED_URL = "https://jobs.appone.com/api/portal/v1/companyjobposts/seahaven"; +const PORTAL_URL = "https://jobs.appone.com/seahaven"; +const ALLOWED_HOSTS = new Set(["apply.appone.com", "jobs.appone.com"]); + +function isAllowedJobUrl(value) { + if (typeof value !== "string") return false; + try { + const url = new URL(value); + return url.protocol === "https:" && ALLOWED_HOSTS.has(url.hostname); + } catch { + return false; + } +} + +function titleCase(value) { + return value + .replace(/[_-]+/g, " ") + .toLowerCase() + .replace(/\b\w/g, (char) => char.toUpperCase()); +} + +function formatSalary(salary) { + if (!salary || salary.salaryOption !== "RANGE" || salary.periodType !== "YEAR") { + return null; + } + const { minimum, maximum } = salary; + if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) return null; + const money = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }); + return `${money.format(minimum)}\u2013${money.format(maximum)} per year`; +} + +function normalizePost(post) { + if (!post || typeof post !== "object") { + throw new Error("Paychex job feed returned a posting that is not an object"); + } + const title = typeof post.jobTitle === "string" ? post.jobTitle.trim() : ""; + if (!title) { + throw new Error("Paychex job feed returned a posting without a title"); + } + if (!isAllowedJobUrl(post.jobPostUrl)) { + throw new Error(`Paychex job feed returned an unexpected apply URL for "${title}"`); + } + return { + title, + url: post.jobPostUrl, + location: typeof post.location === "string" ? post.location.trim() : "", + jobType: typeof post.jobType === "string" ? post.jobType.trim() : "", + workplaceType: typeof post.workplaceType === "string" && post.workplaceType.trim() + ? titleCase(post.workplaceType.trim()) + : "", + salary: formatSalary(post.salary), + }; +} + +module.exports = async function () { + const response = await fetch(FEED_URL, { + headers: { + Accept: "application/json", + "x-payx-bizpn": "Load Company Job Portal", + }, + signal: AbortSignal.timeout(15000), + }); + if (!response.ok) { + throw new Error(`Paychex job feed request failed (${response.status})`); + } + + const data = await response.json(); + if (!data || !Array.isArray(data.jobPosts)) { + throw new Error("Paychex job feed is missing jobPosts"); + } + + return { + portalUrl: PORTAL_URL, + posts: data.jobPosts.map(normalizePost), + }; +}; diff --git a/assets/css/careers.css b/assets/css/careers.css index 5c7097f..bb16794 100644 --- a/assets/css/careers.css +++ b/assets/css/careers.css @@ -95,11 +95,15 @@ color: var(--text); margin-bottom: 0.75rem; } - .job-card > p { + .positions-empty { color: var(--text-muted); - font-size: 0.9375rem; + font-size: 1rem; line-height: 1.7; - margin-bottom: 1.25rem; + max-width: 680px; + } + .positions-empty a { + color: var(--rose); + font-weight: 600; } .job-link { display: inline-flex; diff --git a/assets/css/jobs.css b/assets/css/jobs.css deleted file mode 100644 index 6d1f9b8..0000000 --- a/assets/css/jobs.css +++ /dev/null @@ -1,160 +0,0 @@ -/* ── Job detail (shared: dispatcher/scheduling-coordinator/traveling) (extracted from inline