Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 (
<div className="mx-auto max-w-7xl w-full py-16">
<div className="grid grid-cols-1 min-[440px]:grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-6">
Expand Down Expand Up @@ -220,13 +243,21 @@ function SmallPrint() {
<h3 className="font-mono text-[11px] font-medium uppercase tracking-[0.18em] text-ink-faint mb-4">Company</h3>
<ul className="space-y-3">
{footer.company.map((item) => (
<li key={item.name}>
<li key={item.name} className="flex items-center gap-2">
<a
href={item.href}
className="text-sm text-ink-soft hover:text-ink transition-colors"
>
{item.name}
</a>
{item.href === CAREERS_HREF && openings > 0 && (
<span
className="rounded-full border border-pine/25 bg-pine/[0.06] px-1.5 py-0.5 font-mono text-[10px] font-medium leading-none text-pine"
aria-label={`${openings} open ${openings === 1 ? "position" : "positions"}`}
>
{openings}
</span>
)}
</li>
))}
</ul>
Expand Down Expand Up @@ -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;
Expand All @@ -331,7 +362,7 @@ export function Footer({ initialPathname = "" }) {
<h2 id="footer-heading" className="sr-only">
Footer
</h2>
<SmallPrint />
<SmallPrint initialOpenings={initialOpenings} />
</footer>
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
---

<!DOCTYPE html>
Expand Down Expand Up @@ -201,7 +209,7 @@ const showRssFeed = currentPath.startsWith('/blog') || currentPath.startsWith('/
</Providers>
</TooltipProvider>
<EmbedDetector client:load />
<Footer client:load initialPathname={Astro.url.pathname} />
<Footer client:load initialPathname={Astro.url.pathname} initialOpenings={footerOpenings} />
<CopyCodeScript />
<TabsScript />
<MermaidScript />
Expand Down
22 changes: 22 additions & 0 deletions src/lib/careers.ts
Original file line number Diff line number Diff line change
@@ -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<number | null> {
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;
}
}
Loading