flagon-io/g1t

public

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/apps/web/app/lib/meta.ts

129 lines5,126 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Social cards for every page: og.g1t.sh1/**
2 * Every page's <title> and the tags a link preview reads: its description,
3 * Open Graph and Twitter's card. The image is the page's own card from the
4 * og service (services/og), which draws it as an anonymous visitor would
5 * see the page, so a private page's card never names anything.
6 *
7 * React Router renders only the deepest route's `meta`, so each route
8 * returns `page(args, …)` rather than a bare title.
9 */
10import type { MetaDescriptor } from "react-router";
11
12export const SITE = "https://g1t.sh";
13export const OG = "https://og.g1t.sh";
14
15/** What g1t is, for pages with nothing more particular to say. */
16export const DESCRIPTION =
17 "A git forge for teams of agents: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare.";
18
19/** As much of `MetaArgs` as the tags need. */
20export type PageArgs = {
21 location: { pathname: string };
22 matches: ReadonlyArray<{ id: string; loaderData?: unknown } | undefined>;
23};
24
25export type PageMeta = {
26 /** The whole <title>, ending in "· g1t". */
27 title: string;
28 /** Defaults to the project's or workspace's description, then g1t's. */
29 description?: string | null;
30 /**
31 * Whatever the page's card shows, so that the card is drawn again when it
32 * changes. Defaults to the project's details on project pages.
33 */
34 version?: unknown;
35 type?: "website" | "article";
36};
37
38type ProjectData = {
39 repo?: { description?: string | null };
40 project?: { name?: string; description?: string | null } | null;
41 open?: { issues?: number; pulls?: number };
42};
43type WorkspaceData = { workspace?: { name?: string; description?: string | null } };
44
45function loaded<T>(args: PageArgs, id: string): T | undefined {
46 return args.matches.find((match) => match?.id === id)?.loaderData as T | undefined;
47}
48
49/** A short, stable fingerprint, for the card's `v`. */
50export function fingerprint(value: unknown): string {
51 const text = JSON.stringify(value) ?? "";
52 // FNV-1a, 32 bits.
53 let hash = 0x811c9dc5;
54 for (let i = 0; i < text.length; i++) {
55 hash ^= text.charCodeAt(i);
56 hash = Math.imul(hash, 0x01000193);
57 }
58 return (hash >>> 0).toString(36);
59}
60
61/** A description fit for a preview: one line, at most 200 characters. */
62function summary(text: string): string {
63 const line = text.replace(/\s+/g, " ").trim();
64 return line.length > 200 ? `${line.slice(0, 199).trimEnd()}…` : line;
65}
66
67/** The opening of some Markdown as plain text, for a description. */
68export function excerpt(markdown: string | null | undefined, max = 160): string {
69 const text = (markdown ?? "")
70 .replace(/```[\s\S]*?(```|$)/g, " ")
71 .replace(/<[^>]+>/g, " ")
72 .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
73 .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
74 .replace(/^\s{0,3}(#{1,6}|>|[-*+]|\d+\.)\s+/gm, "")
75 .replace(/[`*_~|]/g, "")
76 .replace(/\s+/g, " ")
77 .trim();
78 return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
79}
80
81/** The address of a page's card. */
82export function cardUrl(pathname: string, version?: unknown): string {
83 const url = new URL("/image", OG);
84 url.searchParams.set("path", pathname);
85 if (version !== undefined) url.searchParams.set("v", fingerprint(version));
86 return url.toString();
87}
88
89export function page(args: PageArgs, meta: PageMeta): MetaDescriptor[] {
90 const project = loaded<ProjectData>(args, "routes/repo/layout");
91 const workspace = loaded<WorkspaceData>(args, "routes/workspace/layout");
92 const description = summary(
93 meta.description ||
94 project?.project?.description ||
95 project?.repo?.description ||
96 workspace?.workspace?.description ||
97 DESCRIPTION,
98 );
99 const version =
100 meta.version ??
101 (project
102 ? [project.project?.name, project.project?.description ?? project.repo?.description, project.open]
103 : workspace
104 ? [workspace.workspace?.name, workspace.workspace?.description]
105 : undefined);
106 const pathname = args.location.pathname.replace(/\/+$/, "") || "/";
107 // The title a preview shows needs no "· g1t": the site name is beside it.
108 const shareTitle = meta.title.replace(/ · g1t$/, "");
109 const image = cardUrl(pathname, version);
110 return [
111 { title: meta.title },
112 { name: "description", content: description },
113 { property: "og:site_name", content: "g1t" },
114 { property: "og:type", content: meta.type ?? "website" },
115 { property: "og:url", content: `${SITE}${pathname === "/" ? "/" : pathname}` },
116 { property: "og:title", content: shareTitle },
117 { property: "og:description", content: description },
118 { property: "og:image", content: image },
119 { property: "og:image:type", content: "image/png" },
120 { property: "og:image:width", content: "1200" },
121 { property: "og:image:height", content: "630" },
122 { property: "og:image:alt", content: shareTitle },
123 { name: "twitter:card", content: "summary_large_image" },
124 { name: "twitter:title", content: shareTitle },
125 { name: "twitter:description", content: description },
126 { name: "twitter:image", content: image },
127 { name: "twitter:image:alt", content: shareTitle },
128 ];
129}