import type { Metadata } from 'next'; import Link from 'next/link'; import { Suspense } from 'react'; import { ActiveFilters, FacetChips, FilterForm, href, parseQuery, titleFor, type SatQuery } from '@/components/satellite/explorer-filters'; import { Derived } from '@/components/satellite/primitives'; import { ResultsTable } from '@/components/satellite/results-table'; import { SearchBox } from '@/components/satellite/search-box'; import { Pagination } from '@/components/ui/pagination'; import { Container, PageHeader } from '@/components/ui/section'; import { Unavailable } from '@/components/ui/unavailable'; import { api, safe } from '@/lib/api'; import { fmtInt } from '@/lib/format'; import { routes, SITE_URL } from '@/lib/site'; const PAGE_SIZE = 50; const FACET_KEYS = ['status', 'object_type', 'orbit_class', 'mission_type', 'country', 'operator', 'constellation', 'on_orbit', 'q'] as const; type Props = { searchParams: Promise> }; function apiQuery(q: SatQuery) { const page = Math.max(1, Number(q.page ?? '1') || 1); return { ...q, page, page_size: PAGE_SIZE, sort: q.sort ?? 'launch_date' }; } function facetQuery(q: SatQuery) { return Object.fromEntries(FACET_KEYS.map((k) => [k, q[k]])); } export async function generateMetadata({ searchParams }: Props): Promise { const q = parseQuery(await searchParams); const filtered = Object.keys(q).filter((k) => k !== 'page' && k !== 'sort').length > 0; // Same request as the page body → deduplicated by the fetch cache, so labels (e.g. "Starlink") are real, not slugs. const facets = filtered ? ((await safe(api.satelliteFacets(facetQuery(q))))?.data ?? null) : null; const title = filtered ? `${titleFor(q, facets)} — Satellite explorer` : 'Satellite explorer — every catalogued object in Earth orbit'; const description = 'Filter 70,000+ catalogued objects by status, object type, orbit class, mission, country, constellation and launch date. Real counts, live element sets, honest gaps.'; const canonical = href(q, {}, false); return { title, description, alternates: { canonical }, robots: q.page || q.q ? { index: false, follow: true } : undefined, openGraph: { title, description, url: `${SITE_URL}${canonical}` }, twitter: { card: 'summary_large_image', title, description }, }; } export default async function SatellitesPage({ searchParams }: Props) { const q = parseQuery(await searchParams); const [list, facetsRes] = await Promise.all([safe(api.satellites(apiQuery(q))), safe(api.satelliteFacets(facetQuery(q)))]); const facets = facetsRes?.data ?? null; const total = list?.pagination.total ?? null; const filtered = Object.keys(q).filter((k) => k !== 'page' && k !== 'sort').length > 0; return ( Catalogue · {total !== null ? {fmtInt(total)} objects : 'count unavailable'}} title={filtered ? titleFor(q, facets) : 'Satellites & catalogued objects'} lede={filtered ? undefined : 'Every object in the public catalogue — payloads, rocket bodies, debris and stations — with live element sets where they exist. Filters are plain URL parameters: share, bookmark, script.'} >
}>

Orbit class, mission and constellation are · methodology

{facets ? : }
{list === null ? ( ) : list.data.length === 0 ? (

No catalogued object matches these filters.

The count is real — nothing is hidden. Try removing a filter{q.launched_after || q.launched_before ? ' (launch-date filters depend on SATCAT launch dates, which are missing for some objects)' : ''}.

Clear all filters
) : ( <> href(q, { page: String(p) }, false)} className="mb-3" /> href(q, { page: String(p) }, false)} className="mt-4" /> )}

Catalogue rows come from CelesTrak SATCAT; perigee, apogee and inclination shown here are catalogue values (the detail page shows the latest element set). Facet counts are computed live over the filtered set.

); }