Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
124 linesCodeBlameRaw
1import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
2import { ogVersion } from '@g1t/contracts/og';
3
4import { TABS, type Tab, tabForGroup } from './tabs';
5
6type Entry = { type: string; label?: string; href?: string; isCurrent?: boolean; entries?: Entry[] };
7
8/** The social cards' service; see services/og. */
9const OG = 'https://og.g1t.sh';
10
11/** The slug of a group's first page on this site, such as `reference/api`. */
12function firstSlug(entries: Entry[]): string | undefined {
13 for (const entry of entries) {
14 if (entry.type === 'group') {
15 const slug = firstSlug(entry.entries ?? []);
16 if (slug !== undefined) return slug;
17 } else if (entry.href?.startsWith('/') && !entry.href.startsWith('//')) {
18 return entry.href.replace(/^\/|\/$/g, '');
19 }
20 }
21 return undefined;
22}
23
24/** The label of the sidebar group holding the current page, the innermost if they nest. */
25function sectionOf(entries: Entry[], group?: string): string | undefined {
26 for (const entry of entries) {
27 if (entry.type === 'group') {
28 const found = sectionOf(entry.entries ?? [], entry.label);
29 if (found !== undefined) return found;
30 } else if (entry.isCurrent) {
31 return group;
32 }
33 }
34 return undefined;
35}
36
37/** What the header and the page's title show: the tabs, and where the page sits. */
38export type Nav = {
39 tabs: { label: string; href: string; current: boolean }[];
40 /** The sidebar group holding the page, or its tab's name, shown above its title. */
41 section?: string;
42};
43
44/** Each route's Nav, set here for Header.astro and PageTitle.astro. */
45export const NAV = new WeakMap<object, Nav>();
46
47/**
48 * The tab a top-level sidebar group belongs to. The API's groups are told
49 * apart by their pages, not their labels, since an API tag can share a
50 * guide group's name.
51 */
52function tabOf(group: Entry): Tab {
53 const slug = firstSlug(group.entries ?? []) ?? '';
54 return slug.startsWith('reference/') ? TABS[TABS.length - 1] : tabForGroup(group.label ?? '');
55}
56
57/**
58 * A tab's groups in the tab's own order (`groups` in tabs.ts), so a tab
59 * opens on, and its sidebar starts with, the group it is named for: Code
60 * on Landing changes, not on Security, which comes first in the sidebar.
61 * The API reference tab names no groups and keeps the sidebar's order.
62 */
63function inTabOrder(groups: Entry[], tab: Tab): Entry[] {
64 const rank = (group: Entry) => {
65 const index = tab.groups.indexOf(group.label ?? '');
66 return index === -1 ? tab.groups.length : index;
67 };
68 return groups.filter((group) => tabOf(group) === tab).sort((a, b) => rank(a) - rank(b));
69}
70
71/** Whether the current page is in these entries, at any depth. */
72function holdsCurrent(entries: Entry[]): boolean {
73 return entries.some((entry) => entry.isCurrent || holdsCurrent(entry.entries ?? []));
74}
75
76/**
77 * Each page's sidebar holds only its tab's groups.
78 *
79 * Each page also gets its own social card: its title, its section and its
80 * description, drawn by the og service.
81 */
82export const onRequest = defineRouteMiddleware((context) => {
83 const route = context.locals.starlightRoute;
84 const groups = (route.sidebar as Entry[]).filter((entry) => entry.type === 'group');
85 const section = sectionOf(groups);
86 // A page in no group, such as the home page, is in the first tab.
87 const holder = groups.find((group) => holdsCurrent(group.entries ?? []));
88 const tab = holder ? tabOf(holder) : TABS[0];
89 // A tab opens on the first page of its first group; the first tab, on the home page.
90 const tabs = TABS.map((each) => {
91 const group = each === TABS[0] ? undefined : inTabOrder(groups, each)[0];
92 const slug = group ? firstSlug(group.entries ?? []) : undefined;
93 return { label: each.label, href: slug === undefined ? '/' : `/${slug}/`, current: each === tab };
94 });
95 NAV.set(route, { tabs, section: section ?? (route.id === '' ? undefined : tab.label) });
96 route.sidebar = inTabOrder(groups, tab) as typeof route.sidebar;
97
98 const { title, description } = route.entry.data;
99 const card = new URL('/docs', OG);
100 card.searchParams.set('title', title);
101 if (section) card.searchParams.set('section', section);
102 if (description) card.searchParams.set('description', description);
103 // The render version, so a new card design is a new address for every cache.
104 card.searchParams.set('v', ogVersion());
105 const image = card.toString();
106 const alt = `${title} · g1t docs`;
107 const meta = (key: 'property' | 'name', name: string, content: string) => ({
108 tag: 'meta' as const,
109 attrs: { [key]: name, content },
110 });
111 // Starlight already writes og:title, og:description, og:url and
112 // twitter:card; these finish the card.
113 route.head.push(
114 meta('property', 'og:image', image),
115 meta('property', 'og:image:type', 'image/png'),
116 meta('property', 'og:image:width', '1200'),
117 meta('property', 'og:image:height', '630'),
118 meta('property', 'og:image:alt', alt),
119 meta('name', 'twitter:title', title),
120 meta('name', 'twitter:image', image),
121 meta('name', 'twitter:image:alt', alt),
122 );
123 if (description) route.head.push(meta('name', 'twitter:description', description));
124});