spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { SitesMap } from '@/components/launches/sites-map';4import { Container, PageHeader } from '@/components/ui/section';5import { Unavailable } from '@/components/ui/unavailable';6import { api, safe } from '@/lib/api';7import { fmtDate, fmtInt, num } from '@/lib/format';8import { routes, SITE_URL } from '@/lib/site';910export const metadata: Metadata = {11 title: 'Launch sites — every spaceport that put an object in orbit',12 description: 'World map and table of launch sites: total launches, activity over the last 365 days, payloads and most recent launch, derived from SATCAT launch-site codes.',13 alternates: { canonical: routes.launchSites() },14 openGraph: { title: 'Launch sites of the world', description: 'Spaceports ranked by orbital launches, with recent activity and last launch.', url: `${SITE_URL}${routes.launchSites()}` },15 twitter: { card: 'summary_large_image', title: 'Launch sites of the world', description: 'Spaceports ranked by orbital launches, with recent activity and last launch.' },16};1718export default async function LaunchSitesPage() {19 const res = await safe(api.launchSites());20 const sites = res ? [...res.data].sort((a, b) => (num(b.launches) ?? 0) - (num(a.launches) ?? 0)) : null;21 const active365 = sites?.filter((s) => (num(s.launches_last_365d) ?? 0) > 0).length ?? null;2223 return (24 <Container wide>25 <PageHeader26 eyebrow={<>Infrastructure · {sites ? <span className="tnum">{fmtInt(sites.length)} sites · {fmtInt(active365)} active in the last 365 days</span> : 'count unavailable'}</>}27 title="Launch sites"28 lede="Every launch site referenced by a catalogued object, from Baikonur in 1957 to today's commercial pads. Marker area is proportional to the number of orbital launches attributed to the site."29 />3031 {sites === null ? (32 <Unavailable what="Launch sites" />33 ) : (34 <>35 <section className="pb-8" aria-label="Map of launch sites">36 <SitesMap sites={sites} labelTop={5} />37 </section>3839 <section className="pb-10" aria-label="Launch sites table">40 <div className="overflow-x-auto scrollbar-thin">41 <table className="data-table stack">42 <thead>43 <tr>44 <th>Site</th>45 <th>Code</th>46 <th>Country</th>47 <th className="num">Launches</th>48 <th className="num">Last 365 d</th>49 <th className="num">Payloads</th>50 <th>First launch</th>51 <th>Last launch</th>52 </tr>53 </thead>54 <tbody>55 {sites.map((s) => {56 const recent = num(s.launches_last_365d) ?? 0;57 return (58 <tr key={s.code}>59 <td data-label="Site" className="primary">60 <Link href={routes.launchSite(s.slug)} className="link font-medium">{s.name}</Link>61 {s.latitude === null && <span className="ml-2 text-2xs text-ink-3">no coordinates</span>}62 </td>63 <td data-label="Code" className="mono text-xs text-ink-2">{s.code}</td>64 <td data-label="Country" className="text-xs">{s.country_code ? <Link href={routes.country(s.country_code)} className="link">{s.country_name ?? s.country_code}</Link> : '—'}</td>65 <td data-label="Launches" className="num mono text-xs">{fmtInt(s.launches)}</td>66 <td data-label="Last 365 d" className={`num mono text-xs ${recent ? 'text-active' : 'text-ink-3'}`}>{fmtInt(s.launches_last_365d)}</td>67 <td data-label="Payloads" className="num mono text-xs">{fmtInt(s.payloads)}</td>68 <td data-label="First launch" className="mono text-xs">{fmtDate(s.first_launch)}</td>69 <td data-label="Last launch" className="mono text-xs">{fmtDate(s.last_launch)}</td>70 </tr>71 );72 })}73 </tbody>74 </table>75 </div>76 </section>77 </>78 )}7980 <p className="pb-10 text-2xs text-ink-3">81 Sites and coordinates come from the SATCAT launch-site code table; launches are derived from international designators (see <Link href={routes.methodology()} className="hover:text-accent">methodology</Link>). Sea launches and air launches are attributed to their reference site.82 </p>83 </Container>84 );85}86