diff --git a/src/components/MetricBar.tsx b/src/components/MetricBar.tsx
index c3fbae3..887ceca 100644
--- a/src/components/MetricBar.tsx
+++ b/src/components/MetricBar.tsx
@@ -41,7 +41,7 @@ export function MetricBar() {
let modes: LeaderMode[] | null = null;
if (routeId === "/") {
- // The company board (?kind=org) ranks by commits only — no metric to pick yet.
+ // The organization board (?kind=org) ranks by commits only — no metric to pick yet.
modes = boardKind === "org" ? null : LEADER_MODES;
} else if (routeId === "/$user") {
if (lookup?.kind === "org") {
diff --git a/src/components/OrgView.tsx b/src/components/OrgView.tsx
index 48c02d0..271e9ca 100644
--- a/src/components/OrgView.tsx
+++ b/src/components/OrgView.tsx
@@ -6,7 +6,7 @@ import type { OrgMemberEntry, OrgResult } from "#/lib/org";
import type { OrgSummary } from "#/lib/org-cache";
/**
- * Org ("company") views rendered by the /$user route when a login resolves to an organization
+ * Organization views rendered by the /$user route when a login resolves to an organization
* (GitHub logins share one namespace, so /paritytech IS the org page). Header + lifetime totals
* of the members' contributions *to this org*, plus the building/error states. No chart yet:
* orgs have no monthly data until the background worker lands (issue #84 follow-up).
@@ -153,6 +153,27 @@ export function OrgResultView({
);
}
+ // Valid but too large to build on demand — recorded and queued for the background worker.
+ // A gentle notice, not a failure: no Retry (retrying can't help), just "check back later".
+ if (result.indexing) {
+ return (
+
+
+ We’re still indexing {result.login}
+
+
{result.indexing}
+
+
+ ← Try another lookup
+
+
+
+ );
+ }
+
// Hard failure, or polling gave up on a stalled build.
const message = result.error
? result.error
@@ -247,7 +268,7 @@ function LoadedOrg({
repositories, as attributed by GitHub. Private members and private
contributions aren’t included.{" "}
@@ -285,7 +306,7 @@ function MemberBoard({
Public members ranked by their lifetime commits to {org.login}’s
repositories.{" "}
diff --git a/src/content/company/members.mdx b/src/content/organizations/members.mdx
similarity index 73%
rename from src/content/company/members.mdx
rename to src/content/organizations/members.mdx
index af9978d..bbb39c8 100644
--- a/src/content/company/members.mdx
+++ b/src/content/organizations/members.mdx
@@ -6,7 +6,7 @@ updatedAt: "2026-07-08"
order: 2
---
-The member leaderboard on a company page lists the organization's **public
+The member leaderboard on an organization page lists the organization's **public
members** — and that's almost always the answer to "why am I not on it":
**GitHub makes organization membership private by default.** Unless you've
explicitly published yours, nobody outside the organization (including us) can
@@ -21,17 +21,17 @@ see that you're a member at all.
GitHub only lets *you* do this for yourself — an org admin can't publish your
membership for you. Once it's public, you'll appear the next time the
-company's data is refreshed, and your contributions start counting toward the
-[company totals](/company/stats) too.
+organization's data is refreshed, and your contributions start counting toward
+the [organization totals](/organizations/stats) too.
## Other reasons someone is missing
- **They're not a member.** Contributing to an org's repositories — even a
lot — doesn't make someone a member. Outside contributors don't appear on
the member list and don't count toward the totals.
-- **The list hasn't refreshed yet.** Membership is captured when the company
- is first looked up and re-synced periodically, so a freshly-publicized
- membership can take a while to show up.
+- **The list hasn't refreshed yet.** Membership is captured when the
+ organization is first looked up and re-synced periodically, so a
+ freshly-publicized membership can take a while to show up.
- **The account is suspended on commit-history.** Profiles under review are
hidden from every leaderboard, member lists included.
@@ -39,6 +39,6 @@ company's data is refreshed, and your contributions start counting toward the
Being on the list with a **0** is normal and not a bug. The numbers are
contributions **to this organization's public repositories** only — see
-[how company stats are calculated](/company/stats). A member shows zero when
-their work in the org happens in private repositories, under an email not
-linked to their GitHub account, or simply in other places entirely.
+[how organization stats are calculated](/organizations/stats). A member shows
+zero when their work in the org happens in private repositories, under an email
+not linked to their GitHub account, or simply in other places entirely.
diff --git a/src/content/company/stats.mdx b/src/content/organizations/stats.mdx
similarity index 59%
rename from src/content/company/stats.mdx
rename to src/content/organizations/stats.mdx
index 7dd4292..cce5f0f 100644
--- a/src/content/company/stats.mdx
+++ b/src/content/organizations/stats.mdx
@@ -1,15 +1,15 @@
---
-title: "How company stats are calculated"
-description: "Where a company's commits, pull requests, reviews and issues come from: each public member's contributions to that organization's repositories, summed — and what that number deliberately leaves out."
+title: "How organization stats are calculated"
+description: "Where an organization's commits, pull requests, reviews and issues come from: each public member's contributions to that organization's repositories, summed — and what that number deliberately leaves out."
publishedAt: "2026-07-08"
updatedAt: "2026-07-08"
order: 1
---
-A company page answers a narrower question than it might look like: **how much
-have this organization's public members publicly contributed to the
+An organization page answers a narrower question than it might look like: **how
+much have this organization's public members publicly contributed to the
organization's own repositories?** Every number on the page — the headline
-stats, the member leaderboard, the company's position on the company
+stats, the member leaderboard, the organization's position on the organization
leaderboard — is built from that one measure.
## The calculation
@@ -24,8 +24,8 @@ Within that scope, GitHub's usual contribution rules apply, the same ones your
personal profile uses: commits count on the default branch, under an email
linked to the account, in non-fork repositories.
-The company totals are simply those per-member numbers **summed**. The member
-leaderboard shows the same rows the sum is made of, so the two always
+The organization totals are simply those per-member numbers **summed**. The
+member leaderboard shows the same rows the sum is made of, so the two always
reconcile.
## What the number is *not*
@@ -35,32 +35,33 @@ things are excluded, and they can make the totals smaller than you'd expect:
- **Private members.** GitHub keeps organization membership private by
default, and we can only see members who made theirs public. An active
- company whose people all keep membership private shows a total of zero —
- see [why am I not on this member list?](/company/members).
+ organization whose people all keep membership private shows a total of zero —
+ see [why am I not on this member list?](/organizations/members).
- **Outside contributors.** Someone who sends pull requests to the org's
repos without being a member isn't counted. The measure is about the
- company's people, not its repos' traffic.
+ organization's people, not its repos' traffic.
- **Private repositories.** We only see public activity. Work in the org's
private repos is invisible to us, even for public members.
-So treat the company numbers as the org's **public footprint**: what its
+So treat the organization numbers as the org's **public footprint**: what its
publicly-listed people have visibly built there.
## How this differs from a personal profile
Your personal page counts your public contributions **across all of GitHub**.
-A company page counts only what landed **in that org's repositories**. That's
-why your number on a member leaderboard is always less than or equal to the
-number on your own profile — often much less, if most of your work happens
+An organization page counts only what landed **in that org's repositories**.
+That's why your number on a member leaderboard is always less than or equal to
+the number on your own profile — often much less, if most of your work happens
elsewhere.
## Freshness and quirks
- Totals cover every **completed month** up to now; the current in-progress
month rolls in once it ends (the same convention as personal profiles).
-- A company's numbers are collected when it's first looked up and refreshed
- periodically after that, so very recent activity can take a while to appear.
-- Company pages don't have a commit chart yet — we currently store lifetime
+- An organization's numbers are collected when it's first looked up and
+ refreshed periodically after that, so very recent activity can take a while
+ to appear.
+- Organization pages don't have a commit chart yet — we currently store lifetime
totals per member, and the per-month history that powers the chart is
collected in a slower background pass.
- In rare cases GitHub's API cannot serve a specific slice of a member's
diff --git a/src/lib/cache.ts b/src/lib/cache.ts
index 07901d4..eb2bae4 100644
--- a/src/lib/cache.ts
+++ b/src/lib/cache.ts
@@ -435,7 +435,7 @@ async function persistEntity(
}
}
-async function recordLookup(database: DB, id: string, now: Date) {
+export async function recordLookup(database: DB, id: string, now: Date) {
try {
await database.insert(lookups).values({ entityId: id, searchedAt: now });
} catch {
diff --git a/src/lib/commit-history.ts b/src/lib/commit-history.ts
index 3d5c125..2dda4b4 100644
--- a/src/lib/commit-history.ts
+++ b/src/lib/commit-history.ts
@@ -115,6 +115,10 @@ export interface RecentEntry {
login: string;
name: string | null;
avatarUrl: string | null;
+ /** Drives the chip's avatar shape (org = square, user = circle) and the verified badge. */
+ kind: "user" | "org";
+ /** Org-only verified badge; null for users. */
+ isVerified: boolean | null;
}
export interface StartPageData {
recent: RecentEntry[];
@@ -220,13 +224,20 @@ async function queryRecent(limit: number): Promise {
login: entities.login,
name: entities.name,
avatarUrl: entities.avatarUrl,
+ kind: entities.kind,
+ isVerified: entities.isVerified,
last: sql`max(${lookups.searchedAt})`,
})
.from(lookups)
.innerJoin(entities, eq(entities.id, lookups.entityId))
- // kind filter is defensive — org lookups aren't recorded (v1), but the strip links every
- // entry to /$user, which for an org login would kick off a bogus user build.
- .where(and(isNull(entities.suspendedAt), eq(entities.kind, "user")))
+ // Users and orgs both belong in the strip (each links to /$user, which resolves either);
+ // repos are the only other kind and don't get a page here, so they're excluded.
+ .where(
+ and(
+ isNull(entities.suspendedAt),
+ inArray(entities.kind, ["user", "org"]),
+ ),
+ )
.groupBy(entities.id)
.orderBy(desc(sql`max(${lookups.searchedAt})`))
.limit(limit);
@@ -234,6 +245,8 @@ async function queryRecent(limit: number): Promise {
login: r.login,
name: r.name,
avatarUrl: r.avatarUrl,
+ kind: r.kind === "org" ? "org" : "user",
+ isVerified: r.isVerified,
}));
}
diff --git a/src/lib/content.ts b/src/lib/content.ts
index b6430f3..c455d5d 100644
--- a/src/lib/content.ts
+++ b/src/lib/content.ts
@@ -1,9 +1,9 @@
/**
* The MDX content collections:
* - src/content/metrics/.mdx → /metrics/ (individual metrics, /metrics/explained hub)
- * - src/content/company/.mdx → /company/ (company/org context — deliberately its own
- * collection, NOT mixed into the individuals' hub: the definitions differ, e.g. org-scoped vs
- * global contributions)
+ * - src/content/organizations/.mdx → /organizations/ (organization context —
+ * deliberately its own collection, NOT mixed into the individuals' hub: the definitions differ,
+ * e.g. org-scoped vs global contributions)
*
* Two globs per collection with different costs:
* - an eager, frontmatter-only glob (tiny — just the exported metadata objects), used
@@ -69,21 +69,19 @@ export function loadArticle(
return load?.();
}
-// ── Company collection (/company/) ─────────────────────────────────────
+// ── Organization collection (/organizations/) ──────────────────────────
-const companyFrontmatters = import.meta.glob(
- "../content/company/*.mdx",
+const orgFrontmatters = import.meta.glob(
+ "../content/organizations/*.mdx",
{ eager: true, import: "frontmatter" },
);
-const companyComponents = import.meta.glob<{ default: MDXContent }>(
- "../content/company/*.mdx",
+const orgComponents = import.meta.glob<{ default: MDXContent }>(
+ "../content/organizations/*.mdx",
);
-/** All company articles in curated reading order. */
-export const companyArticles: ArticleMeta[] = Object.entries(
- companyFrontmatters,
-)
+/** All organization articles in curated reading order. */
+export const orgArticles: ArticleMeta[] = Object.entries(orgFrontmatters)
.map(([path, fm]) => ({ slug: slugOf(path), ...fm }))
.sort(
(a, b) =>
@@ -91,14 +89,14 @@ export const companyArticles: ArticleMeta[] = Object.entries(
(b.order ?? Number.MAX_SAFE_INTEGER) || a.title.localeCompare(b.title),
);
-export function getCompanyArticleMeta(slug: string): ArticleMeta | undefined {
- return companyArticles.find((a) => a.slug === slug);
+export function getOrgArticleMeta(slug: string): ArticleMeta | undefined {
+ return orgArticles.find((a) => a.slug === slug);
}
-/** Lazily import a company article's compiled MDX module (shape fits React.lazy). */
-export function loadCompanyArticle(
+/** Lazily import an organization article's compiled MDX module (shape fits React.lazy). */
+export function loadOrgArticle(
slug: string,
): Promise<{ default: MDXContent }> | undefined {
- const load = companyComponents[`../content/company/${slug}.mdx`];
+ const load = orgComponents[`../content/organizations/${slug}.mdx`];
return load?.();
}
diff --git a/src/lib/org-cache.ts b/src/lib/org-cache.ts
index 6dd00f0..4edeed0 100644
--- a/src/lib/org-cache.ts
+++ b/src/lib/org-cache.ts
@@ -1,4 +1,5 @@
import { and, eq, isNull, sql } from "drizzle-orm";
+import { recordLookup } from "#/lib/cache";
import { type DB, db } from "#/lib/db";
import { entities, orgMembers } from "#/lib/db/schema";
import {
@@ -12,12 +13,13 @@ import {
} from "#/lib/github";
/**
- * Incremental org ("company") cache — the org sibling of cache.ts.
+ * Incremental organization cache — the org sibling of cache.ts.
*
* An org's numbers are the sum of its members' contributions *to that org* (org-scoped
* `contributionsCollection(organizationID: …)`, a different number from each member's global
* totals). Those per-member sums live in `org_members`; the roll-up lands on the org's
- * `entities` row so the company leaderboard ranks orgs exactly like the user board ranks users.
+ * `entities` row so the organization leaderboard ranks orgs exactly like the user board ranks
+ * users.
*
* Builds are **resumable** at member granularity: enumeration inserts every member as a pending
* `org_members` row (`lastFetched` null), each fetched member is stamped the moment its totals
@@ -43,9 +45,10 @@ const BUILD_BUDGET_MS = 6_000;
const MEMBER_CONCURRENCY = 3;
// Orgs with more visible members than this get a clean refusal instead of an on-demand build:
-// at ~2 requests per member a mega-org lookup would eat the shared token's hourly budget and
-// poison it for every visitor. Lifting this needs the background worker.
-const MAX_ORG_MEMBERS = 400;
+// at ~2 requests per member even a mid-size org lookup can eat into the shared token's hourly
+// budget and poison it for every visitor. We keep this deliberately low and record the refused
+// orgs (see getFromDb) so the background worker can backfill them later, off the request path.
+const MAX_ORG_MEMBERS = 25;
export interface OrgSummary {
login: string;
@@ -141,11 +144,19 @@ async function getFromDb(
if (!row) {
// First sighting: the profile fetch validates the login and yields the node id (keys every
- // org-scoped contribution query) + createdAt (caps member windows). Refuse oversized orgs
- // BEFORE writing anything — a half-enumerated mega-org would burn quota on every poll.
+ // org-scoped contribution query) + createdAt (caps member windows). Record the profile
+ // (a single cheap upsert into `entities`, builtAt still null) BEFORE refusing an oversized
+ // org — this leaves a tracked row the background worker can later backfill, instead of
+ // discarding the lookup. The request path still won't build it live.
const profile = await fetchOrgProfile(login, token);
- assertBuildable(profile);
row = await upsertOrgProfile(database, id, profile, now);
+ // Record the lookup before the size check so even an oversized org we refuse to build
+ // still surfaces in "recently looked up" — the row exists and the strip links to /$user.
+ await recordLookup(database, id, now);
+ assertBuildable(profile);
+ } else {
+ // Known org, revisited: bump its recency so it re-sorts to the front of the strip.
+ await recordLookup(database, id, now);
}
const complete = row.builtAt != null;
@@ -183,6 +194,13 @@ async function getFromDb(
throw new GitHubError(`Could not resolve "${login}" on GitHub.`, 502);
}
+ // Enrolled but oversized: the row is recorded for the worker, but the request path must refuse
+ // it — cheaply, from the stored memberCount, so a repeat lookup never pays to re-enumerate its
+ // members before bailing. (assertBuildable already caught it at first sighting.)
+ if ((row.memberCount ?? 0) > MAX_ORG_MEMBERS) {
+ throw new GitHubError(tooLargeMessage(login, row.memberCount ?? 0), 422);
+ }
+
// Enumerate members once per build: every member becomes a pending org_members row, which
// doubles as the resume marker. Re-enumeration (membership drift) is the worker's job.
if ((await memberRowCount(database, id)) === 0) {
@@ -278,7 +296,7 @@ function assertBuildable(profile: OrgProfile) {
}
function tooLargeMessage(login: string, count: number) {
- return `"${login}" has ${count.toLocaleString()} public members — too many for an on-demand build yet.`;
+ return `${login} has ${count.toLocaleString()} public members — a bit much for us to crunch on the spot. We’ve added it to the queue and are indexing it in the background; check back a little later. Thanks for your interest!`;
}
async function memberRowCount(database: DB, id: string): Promise {
diff --git a/src/lib/org.ts b/src/lib/org.ts
index da09fdb..d77a519 100644
--- a/src/lib/org.ts
+++ b/src/lib/org.ts
@@ -12,7 +12,7 @@ import { type BuildProgress, GitHubError } from "#/lib/github";
import { getOrgSummary, type OrgSummary, orgEntityId } from "#/lib/org-cache";
/**
- * Server functions for org ("company") pages and the company leaderboard. Split from
+ * Server functions for organization pages and the organization leaderboard. Split from
* commit-history.ts to keep that module user-only; same createServerFn conventions (and the
* same "don't rename to *.server.ts" caveat documented there).
*/
@@ -38,10 +38,14 @@ export interface OrgResult {
// Non-null while the initial server-side build is still in progress — each poll of the loader
// advances it (progress counts *members*, not months). Mutually exclusive with `error`.
building: BuildProgress | null;
+ // Non-null when the org is valid but too large for an on-demand build: it's been recorded and
+ // the background worker will fill it in. Carries a friendly "still indexing" message, and the
+ // UI shows a gentle notice rather than a failure card. Mutually exclusive with the fields above.
+ indexing: string | null;
}
/**
- * A company's members ranked by their commits *to that org* — the same rows the org's totals
+ * An organization's members ranked by their commits *to that org* — the same rows the org's totals
* are summed from, so the list always reconciles with the headline numbers. Suspended members
* are hidden (consistent with every other board); not-yet-fetched members (mid-build) carry
* no numbers yet and are skipped.
@@ -77,7 +81,7 @@ async function resolveOrg(login: string): Promise {
const org = await getOrgSummary(login, serverToken());
// Best-effort: a members-query hiccup must not drop an already-loaded org page.
const members = await queryOrgMembers(orgEntityId(login)).catch(() => []);
- return { login, org, members, error: null, building: null };
+ return { login, org, members, error: null, building: null, indexing: null };
} catch (e) {
// The 503 "still building" rejection carries progress — surface it as `building` so the
// client polls to continue instead of showing a failure card (same mapping as the user
@@ -86,16 +90,22 @@ async function resolveOrg(login: string): Promise {
e instanceof GitHubError && e.status === 503 && e.progress
? e.progress
: null;
+ // 422 = valid but too large to build on demand. It's been recorded for the background
+ // worker, so this isn't a failure — surface it as a friendly "still indexing" notice.
+ const indexing =
+ e instanceof GitHubError && e.status === 422 ? e.message : null;
return {
login,
org: null,
members: [],
- error: building
- ? null
- : e instanceof Error
- ? e.message
- : "Failed to load",
+ error:
+ building || indexing
+ ? null
+ : e instanceof Error
+ ? e.message
+ : "Failed to load",
building,
+ indexing,
};
}
}
@@ -145,7 +155,7 @@ export const getLookup = createServerFn({ method: "GET" })
if (solo) {
const kinds = await knownKinds(solo);
// Both kinds existing at once means a login changed hands across a rename — prefer the
- // user row (the historical default); the org stays reachable via the company board.
+ // user row (the historical default); the org stays reachable via the organization board.
if (kinds.has("org") && !kinds.has("user")) {
return { kind: "org", org: await resolveOrg(solo) };
}
@@ -157,7 +167,7 @@ export const getLookup = createServerFn({ method: "GET" })
/could not resolve to a user/i.test(users[0].error)
) {
const org = await resolveOrg(solo);
- if (org.org || org.building) return { kind: "org", org };
+ if (org.org || org.building || org.indexing) return { kind: "org", org };
// The login isn't a user. If the org path failed for any reason other than "no such
// org" (token scopes, a GitHub hiccup mid-build), that error is the truthful one —
// surface it instead of the misleading user 404. A login that is neither keeps the
@@ -169,7 +179,7 @@ export const getLookup = createServerFn({ method: "GET" })
return { kind: "users", users };
});
-export interface CompanyLeaderEntry {
+export interface OrgLeaderEntry {
login: string;
name: string | null;
avatarUrl: string | null;
@@ -184,14 +194,14 @@ export interface CompanyLeaderEntry {
}
/**
- * Companies ranked by their members' org-scoped commits. Only fully built orgs appear — a
+ * Organizations ranked by their members' org-scoped commits. Only fully built orgs appear — a
* half-built org's totals are still zero/partial and would rank nonsense. v1 ranks by commits
* only; the per-metric machinery can follow once orgs get their own metric bar.
*/
-async function queryCompanyLeaderboard(
+async function queryOrgLeaderboard(
offset: number,
limit: number,
-): Promise {
+): Promise {
if (!db) return [];
return (
db
@@ -221,11 +231,11 @@ async function queryCompanyLeaderboard(
);
}
-/** One page of the company leaderboard — same clamp conventions as getLeaderboard. */
-export const getCompanyLeaderboard = createServerFn({ method: "GET" })
+/** One page of the organization leaderboard — same clamp conventions as getLeaderboard. */
+export const getOrgLeaderboard = createServerFn({ method: "GET" })
.validator((p: { offset: number; limit: number }) => p)
- .handler(({ data }): Promise => {
+ .handler(({ data }): Promise => {
const offset = Math.min(Math.max(0, data.offset), LEADERBOARD_MAX);
const limit = Math.min(Math.max(0, data.limit), LEADERBOARD_MAX - offset);
- return queryCompanyLeaderboard(offset, limit);
+ return queryOrgLeaderboard(offset, limit);
});
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 7c7dc7f..01f87f8 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as UserRouteImport } from './routes/$user'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as OrganizationsSlugRouteImport } from './routes/organizations.$slug'
import { Route as MetricsExplainedRouteImport } from './routes/metrics.explained'
import { Route as MetricsSlugRouteImport } from './routes/metrics.$slug'
import { Route as EmbedUserRouteImport } from './routes/embed.$user'
@@ -26,6 +27,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const OrganizationsSlugRoute = OrganizationsSlugRouteImport.update({
+ id: '/organizations/$slug',
+ path: '/organizations/$slug',
+ getParentRoute: () => rootRouteImport,
+} as any)
const MetricsExplainedRoute = MetricsExplainedRouteImport.update({
id: '/metrics/explained',
path: '/metrics/explained',
@@ -54,6 +60,7 @@ export interface FileRoutesByFullPath {
'/embed/$user': typeof EmbedUserRoute
'/metrics/$slug': typeof MetricsSlugRoute
'/metrics/explained': typeof MetricsExplainedRoute
+ '/organizations/$slug': typeof OrganizationsSlugRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
@@ -62,6 +69,7 @@ export interface FileRoutesByTo {
'/embed/$user': typeof EmbedUserRoute
'/metrics/$slug': typeof MetricsSlugRoute
'/metrics/explained': typeof MetricsExplainedRoute
+ '/organizations/$slug': typeof OrganizationsSlugRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -71,6 +79,7 @@ export interface FileRoutesById {
'/embed/$user': typeof EmbedUserRoute
'/metrics/$slug': typeof MetricsSlugRoute
'/metrics/explained': typeof MetricsExplainedRoute
+ '/organizations/$slug': typeof OrganizationsSlugRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -81,6 +90,7 @@ export interface FileRouteTypes {
| '/embed/$user'
| '/metrics/$slug'
| '/metrics/explained'
+ | '/organizations/$slug'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -89,6 +99,7 @@ export interface FileRouteTypes {
| '/embed/$user'
| '/metrics/$slug'
| '/metrics/explained'
+ | '/organizations/$slug'
id:
| '__root__'
| '/'
@@ -97,6 +108,7 @@ export interface FileRouteTypes {
| '/embed/$user'
| '/metrics/$slug'
| '/metrics/explained'
+ | '/organizations/$slug'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -106,6 +118,7 @@ export interface RootRouteChildren {
EmbedUserRoute: typeof EmbedUserRoute
MetricsSlugRoute: typeof MetricsSlugRoute
MetricsExplainedRoute: typeof MetricsExplainedRoute
+ OrganizationsSlugRoute: typeof OrganizationsSlugRoute
}
declare module '@tanstack/react-router' {
@@ -124,6 +137,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/organizations/$slug': {
+ id: '/organizations/$slug'
+ path: '/organizations/$slug'
+ fullPath: '/organizations/$slug'
+ preLoaderRoute: typeof OrganizationsSlugRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/metrics/explained': {
id: '/metrics/explained'
path: '/metrics/explained'
@@ -162,6 +182,7 @@ const rootRouteChildren: RootRouteChildren = {
EmbedUserRoute: EmbedUserRoute,
MetricsSlugRoute: MetricsSlugRoute,
MetricsExplainedRoute: MetricsExplainedRoute,
+ OrganizationsSlugRoute: OrganizationsSlugRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/routes/$user.tsx b/src/routes/$user.tsx
index 90372d2..218e91f 100644
--- a/src/routes/$user.tsx
+++ b/src/routes/$user.tsx
@@ -60,7 +60,7 @@ export const Route = createFileRoute("/$user")({
const logins = parseLogins(params.user);
const isOrg = loaderData?.kind === "org";
const title = isOrg
- ? `${logins[0]} — company commit history`
+ ? `${logins[0]} — organization commit history`
: logins.length > 1
? `${logins.join(" vs ")} — commit history`
: `${logins[0] ?? "GitHub user"}’s commit history`;
diff --git a/src/routes/company.$slug.tsx b/src/routes/company.$slug.tsx
index cd66226..117b69b 100644
--- a/src/routes/company.$slug.tsx
+++ b/src/routes/company.$slug.tsx
@@ -1,144 +1,17 @@
-import { createFileRoute, Link, notFound } from "@tanstack/react-router";
-import { type ComponentType, lazy, Suspense } from "react";
-import { mdxComponents } from "#/components/MdxComponents";
-import { getCompanyArticleMeta, loadCompanyArticle } from "#/lib/content";
+import { createFileRoute, redirect } from "@tanstack/react-router";
/**
- * Company-context explainer articles (src/content/company/*.mdx) — the org twins of the
- * /metrics/$slug pages. A separate collection on purpose: company numbers are org-scoped and
- * public-member-only, so their definitions differ from the individual metric explainers.
- * Same URL policy as /metrics: bare /company stays a valid GitHub login on the $user route.
- * No per-article OG images (yet) — these fall back to the site-wide card.
+ * Permanent redirect from the old /company/ explainer URLs to their new /organizations/
+ * home (the collection was renamed when "companies" became "organizations" site-wide). Kept as a
+ * thin route so existing links and search-engine results don't 404. Bare /company still falls
+ * through to the $user route as a valid GitHub login — same policy as /organizations.
*/
-
-const SITE = "https://commit-history.com";
-
-// One stable lazy component per slug — created on demand, cached at module level so
-// re-renders don't recreate (and thereby remount) the article body.
-type BodyComponent = ComponentType<{ components?: typeof mdxComponents }>;
-const bodies = new Map();
-function articleBody(slug: string): BodyComponent {
- let body = bodies.get(slug);
- if (!body) {
- body = lazy(() => {
- const load = loadCompanyArticle(slug);
- if (!load) throw notFound();
- return load;
- });
- bodies.set(slug, body);
- }
- return body;
-}
-
-function formatDate(iso: string) {
- return new Date(iso).toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- day: "numeric",
- });
-}
-
export const Route = createFileRoute("/company/$slug")({
- loader: ({ params }) => {
- const meta = getCompanyArticleMeta(params.slug);
- if (!meta) throw notFound();
- return meta;
- },
- head: ({ params }) => {
- const meta = getCompanyArticleMeta(params.slug);
- if (!meta) return {};
- const url = `${SITE}/company/${meta.slug}`;
- return {
- meta: [
- { title: `${meta.title} · Commit History` },
- { name: "description", content: meta.description },
- { property: "og:type", content: "article" },
- { property: "og:title", content: meta.title },
- { property: "og:description", content: meta.description },
- { property: "og:url", content: url },
- { property: "article:published_time", content: meta.publishedAt },
- { property: "article:modified_time", content: meta.updatedAt },
- { name: "twitter:title", content: meta.title },
- { name: "twitter:description", content: meta.description },
- ],
- links: [{ rel: "canonical", href: url }],
- scripts: [
- {
- type: "application/ld+json",
- children: JSON.stringify([
- {
- "@context": "https://schema.org",
- "@type": "TechArticle",
- headline: meta.title,
- description: meta.description,
- datePublished: meta.publishedAt,
- dateModified: meta.updatedAt,
- mainEntityOfPage: url,
- author: {
- "@type": "Person",
- name: "Philip Poloczek",
- url: "https://github.com/peetzweg",
- },
- publisher: {
- "@type": "Organization",
- name: "Commit History",
- url: SITE,
- logo: {
- "@type": "ImageObject",
- url: `${SITE}/crown-180.png`,
- },
- },
- },
- {
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Commit History",
- item: SITE,
- },
- { "@type": "ListItem", position: 2, name: meta.title },
- ],
- },
- ]),
- },
- ],
- };
+ beforeLoad: ({ params }) => {
+ throw redirect({
+ to: "/organizations/$slug",
+ params: { slug: params.slug },
+ statusCode: 301,
+ });
},
- component: ArticlePage,
});
-
-function ArticlePage() {
- const meta = Route.useLoaderData();
- const Body = articleBody(meta.slug);
- return (
-
-
-
-
-
{meta.title}
-
{meta.description}
-
- Updated{" "}
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 53c800f..571efe3 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -13,7 +13,7 @@ import {
type LeaderMode,
type RecentEntry,
} from "#/lib/commit-history";
-import { type CompanyLeaderEntry, getCompanyLeaderboard } from "#/lib/org";
+import { getOrgLeaderboard, type OrgLeaderEntry } from "#/lib/org";
import { cn } from "#/lib/utils";
// Leaderboard metrics that live in the URL as `?metric=…`. "public" (commits) is the default and
@@ -35,22 +35,17 @@ function isLeaderMetricParam(v: unknown): v is LeaderMode {
interface HomeSearch {
/** Selected leaderboard metric; absent = the default (commits). */
metric?: LeaderMode;
- /** Which board is shown; absent = developers, "org" = the company board. */
+ /** Which board is shown; absent = developers, "org" = the organization board. */
kind?: "org";
}
-// Company board page size — a single page for now (the board is young); swap for the
+// Organization board page size — a single page for now (the board is young); swap for the
// LEADERBOARD_PAGE_STOPS infinite-scroll pattern when it outgrows this.
-const COMPANY_PAGE_SIZE = 100;
-
-// Feature flag: the company board ships dark — fully working but only reachable via a direct
-// `/?kind=org` URL, so the board can be filled and checked in production before it's
-// advertised. Flip to true to show the Developers/Companies switch to everyone.
-const SHOW_BOARD_TOGGLE = false;
+const ORG_PAGE_SIZE = 100;
export const Route = createFileRoute("/")({
// `?metric=` selects the leaderboard type so a view can be shared; invalid/absent → commits.
- // `?kind=org` flips the board to companies (same clean-URL convention: default is absent).
+ // `?kind=org` flips the board to organizations (same clean-URL convention: default is absent).
validateSearch: (search: Record): HomeSearch => ({
...(isLeaderMetricParam(search.metric) ? { metric: search.metric } : {}),
...(search.kind === "org" ? { kind: "org" as const } : {}),
@@ -62,16 +57,16 @@ export const Route = createFileRoute("/")({
loaderDeps: ({ search }) => ({ kind: search.kind }),
loader: async ({ deps }) => {
if (deps.kind === "org") {
- const [recent, companies] = await Promise.all([
+ const [recent, orgs] = await Promise.all([
getRecentLookups(),
- getCompanyLeaderboard({
- data: { offset: 0, limit: COMPANY_PAGE_SIZE },
+ getOrgLeaderboard({
+ data: { offset: 0, limit: ORG_PAGE_SIZE },
}),
]);
- return { recent, leaderboard: [] as LeaderEntry[], companies };
+ return { recent, leaderboard: [] as LeaderEntry[], orgs };
}
const start = await getStartPageData();
- return { ...start, companies: [] as CompanyLeaderEntry[] };
+ return { ...start, orgs: [] as OrgLeaderEntry[] };
},
component: Home,
});
@@ -136,13 +131,11 @@ function Home() {
{recent.length > 0 && }
- {SHOW_BOARD_TOGGLE && (
-
-
-
- )}
+
+
+
{kind === "org" ? (
-
+
) : (
initial.leaderboard.length > 0 && (
@@ -162,9 +155,10 @@ function Home() {
}
/**
- * Centered tab bar above the leaderboard heading, switching between the developer and company
- * boards via `?kind=`. Switching also drops `?metric=` — the company board ranks by commits
- * only (per-metric company boards can follow), so a stale metric param would be meaningless.
+ * Centered tab bar above the leaderboard heading, switching between the developer and
+ * organization boards via `?kind=`. Switching also drops `?metric=` — the organization board
+ * ranks by commits only (per-metric org boards can follow), so a stale metric param would be
+ * meaningless.
*/
function BoardKindToggle() {
const { kind } = Route.useSearch();
@@ -185,7 +179,7 @@ function BoardKindToggle() {
{(
[
["user", "Developers"],
- ["org", "Companies"],
+ ["org", "Organizations"],
] as const
).map(([k, label]) => (