diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
index 27bfbd0..faba171 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.jsx
@@ -1,5 +1,7 @@
"use client";
+import { useEffect, useState } from "react";
import { usePathname } from "@/hooks/usePathname";
+import { fetchOpeningsCount } from "@/lib/careers";
import { Button } from "@/components/Button";
import routes from "@/generated/routes.json";
import clsx from "clsx";
@@ -137,7 +139,28 @@ export function PageNextPrevious({ navigation }) {
);
}
-function SmallPrint() {
+// Live opening count for the Careers pill. `initialOpenings` is fetched
+// server-side (BaseLayout) so the pill is in the initial HTML — no dependency on
+// the visitor's browser reaching workatastartup.com. The client refresh keeps a
+// statically-built page current; a blocked or failed refresh keeps the seed.
+const CAREERS_HREF = "/careers";
+
+function useOpeningsCount(initial) {
+ const [count, setCount] = useState(initial ?? null);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ fetchOpeningsCount(controller.signal).then((next) => {
+ if (next !== null) setCount(next);
+ });
+ return () => controller.abort();
+ }, []);
+
+ return count;
+}
+
+function SmallPrint({ initialOpenings }) {
+ const openings = useOpeningsCount(initialOpenings);
return (
@@ -220,13 +243,21 @@ function SmallPrint() {
Company
{footer.company.map((item) => (
- -
+
-
{item.name}
+ {item.href === CAREERS_HREF && openings > 0 && (
+
+ {openings}
+
+ )}
))}
@@ -308,7 +339,7 @@ function SmallPrint() {
// a dark override wrapper instead.
const DARK_THEMED_PATH_PREFIXES = ['/learn'];
-export function Footer({ initialPathname = "" }) {
+export function Footer({ initialPathname = "", initialOpenings = null }) {
// usePathname returns "" during SSR; fall back to the server-provided path
// so docs pages do not flash a light footer before hydration.
const pathname = usePathname() || initialPathname;
@@ -331,7 +362,7 @@ export function Footer({ initialPathname = "" }) {
-
+
);
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index fee77ba..2260e9d 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -4,6 +4,7 @@ import '@fortawesome/fontawesome-svg-core/styles.css';
import { ViewTransitions } from 'astro:transitions';
import { EmbedDetector } from '@/components/EmbedDetector';
import { Footer } from '@/components/Footer';
+import { fetchOpeningsCount } from '@/lib/careers';
import { Providers } from '@/components/Providers';
import { TooltipProvider } from '@rivet-gg/components';
import CopyCodeScript from '@/components/CopyCodeScript.astro';
@@ -68,6 +69,13 @@ const effectiveCanonicalUrl = canonicalUrl || `https://rivet.dev${ensureTrailing
// RSS feed is only relevant for blog and changelog pages
const showRssFeed = currentPath.startsWith('/blog') || currentPath.startsWith('/changelog');
+
+// Open-role count for the footer Careers pill, fetched server-side so the pill
+// is baked into the HTML and does not depend on the visitor's browser reaching
+// workatastartup.com. Bounded so a slow feed cannot stall the response; a null
+// result (timeout/error) simply renders no pill. The Footer refreshes this
+// client-side to keep statically-built pages current.
+const footerOpenings = await fetchOpeningsCount(AbortSignal.timeout(4000));
---
@@ -201,7 +209,7 @@ const showRssFeed = currentPath.startsWith('/blog') || currentPath.startsWith('/
-
+
diff --git a/src/lib/careers.ts b/src/lib/careers.ts
new file mode 100644
index 0000000..6aab957
--- /dev/null
+++ b/src/lib/careers.ts
@@ -0,0 +1,22 @@
+// Single source for the Work at a Startup feed that powers both the /careers
+// job board and the footer Careers pill. Keeping the URL and the shape logic
+// here stops the server-side fetch (BaseLayout) and the client-side refresh
+// (Footer) from drifting apart.
+export const WAAS_JOBS_URL = "https://www.workatastartup.com/embed/rivet/jobs";
+
+// Returns the number of open roles, or null on any failure (network error, bad
+// status, unexpected shape). Callers render no pill when the count is null.
+// The feed sends `access-control-allow-origin: *`, so this works from the
+// browser as well as the server.
+export async function fetchOpeningsCount(
+ signal?: AbortSignal,
+): Promise
{
+ try {
+ const res = await fetch(WAAS_JOBS_URL, signal ? { signal } : undefined);
+ if (!res.ok) return null;
+ const data = await res.json();
+ return Array.isArray(data?.jobs) ? data.jobs.length : null;
+ } catch {
+ return null;
+ }
+}