| 1 | /** |
| 2 | * The dispatcher for g1t.page: every app deployed by g1t is served here. |
| 3 | * |
| 4 | * The hostname's first label is the app's script name in the Workers for |
| 5 | * Platforms namespace (`pr-12--web--acme.g1t.page` is pull request 12's |
| 6 | * preview of acme/web), so a request needs no lookup: the app is fetched |
| 7 | * by name and runs only for as long as it answers. An app no one visits |
| 8 | * runs nothing and costs nothing. |
| 9 | * |
| 10 | * Kept apart from g1t.sh, so apps share no cookies or origin with the site |
| 11 | * people sign in to. |
| 12 | */ |
| 13 | |
| 14 | type Env = { |
| 15 | APPS: DispatchNamespace; |
| 16 | }; |
| 17 | |
| 18 | const DOMAIN = "g1t.page"; |
| 19 | |
| 20 | function page(status: number, title: string, body: string): Response { |
| 21 | const html = `<!doctype html> |
| 22 | <html lang="en"> |
| 23 | <head> |
| 24 | <meta charset="utf-8"> |
| 25 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 26 | <meta name="robots" content="noindex"> |
| 27 | <title>${title} · g1t.page</title> |
| 28 | <style> |
| 29 | :root { color-scheme: dark; --bg: #121214; --fg: #ececf1; --muted: #9a9aa6; --accent: #b4a7f5; } |
| 30 | body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg); |
| 31 | font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; padding: 0 16px; } |
| 32 | main { max-width: 32rem; } |
| 33 | h1 { font-size: 1.5rem; margin: 0 0 .5rem; letter-spacing: -.01em; } |
| 34 | p { color: var(--muted); margin: .5rem 0; } |
| 35 | a { color: var(--accent); } |
| 36 | code { font: .9em ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--fg); } |
| 37 | </style> |
| 38 | </head> |
| 39 | <body><main><h1>${title}</h1>${body}</main></body> |
| 40 | </html>`; |
| 41 | return new Response(html, { |
| 42 | status, |
| 43 | headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex" }, |
| 44 | }); |
| 45 | } |
| 46 | |
| 47 | function escape(text: string): string { |
| 48 | return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`); |
| 49 | } |
| 50 | |
| 51 | export default { |
| 52 | async fetch(request: Request, env: Env): Promise<Response> { |
| 53 | const host = new URL(request.url).hostname.toLowerCase(); |
| 54 | if (host === DOMAIN) { |
| 55 | return page( |
| 56 | 200, |
| 57 | "g1t.page", |
| 58 | `<p>Apps deployed by <a href="https://g1t.sh">g1t</a>: every pull request's preview, and each repository's production.</p> |
| 59 | <p><a href="https://docs.g1t.sh/guides/deployments/">How deployments work</a></p>`, |
| 60 | ); |
| 61 | } |
| 62 | const label = host.endsWith(`.${DOMAIN}`) ? host.slice(0, -DOMAIN.length - 1) : ""; |
| 63 | if (!/^[a-z0-9-]{1,63}$/.test(label)) { |
| 64 | return page(404, "Nothing here", "<p>This is not the address of an app on g1t.page.</p>"); |
| 65 | } |
| 66 | let app: Fetcher; |
| 67 | try { |
| 68 | app = env.APPS.get(label); |
| 69 | } catch { |
| 70 | return missing(label); |
| 71 | } |
| 72 | let response: Response; |
| 73 | try { |
| 74 | response = await app.fetch(request); |
| 75 | } catch (error) { |
| 76 | if (/worker not found|script not found|does not exist/i.test(String(error))) return missing(label); |
| 77 | return page( |
| 78 | 502, |
| 79 | "The app failed", |
| 80 | `<p>The app at <code>${escape(host)}</code> threw an error before it could answer.</p>`, |
| 81 | ); |
| 82 | } |
| 83 | // Previews are for the people reviewing a change, not search engines. |
| 84 | if (label.startsWith("pr-")) { |
| 85 | response = new Response(response.body, response); |
| 86 | response.headers.set("x-robots-tag", "noindex"); |
| 87 | } |
| 88 | return response; |
| 89 | }, |
| 90 | } satisfies ExportedHandler<Env>; |
| 91 | |
| 92 | function missing(label: string): Response { |
| 93 | const preview = label.startsWith("pr-"); |
| 94 | return page( |
| 95 | 404, |
| 96 | preview ? "This preview is not up" : "Nothing deployed here", |
| 97 | preview |
| 98 | ? `<p>A preview comes down when its pull request is closed or merged, or after its repository's idle days without a visit. Pushing to the pull request, or redeploying it from the repository's Deployments page, brings it back.</p>` |
| 99 | : `<p>No app is deployed at <code>${escape(label)}.${DOMAIN}</code>. Deployments are turned on per repository, from its Deployments page on g1t.</p>`, |
| 100 | ); |
| 101 | } |