|
| 1 | +// Host-based edge routing: |
| 2 | +// docs.<domain> -> reverse-proxy to the public Notion site |
| 3 | +// <domain> / www. -> DevArts Lab landing page |
| 4 | +// mail.<domain> / workers.dev -> the mail app (handled by caller) |
| 5 | + |
| 6 | +import type { Env } from "./types"; |
| 7 | + |
| 8 | +const NOTION_ORIGIN = "https://www.notion.so"; |
| 9 | + |
| 10 | +const DROP_RESPONSE_HEADERS = new Set([ |
| 11 | + "content-encoding", |
| 12 | + "content-length", |
| 13 | + "transfer-encoding", |
| 14 | + "content-security-policy", |
| 15 | + "content-security-policy-report-only", |
| 16 | + "set-cookie", |
| 17 | + "x-frame-options", |
| 18 | + "report-to", |
| 19 | + "nel", |
| 20 | +]); |
| 21 | + |
| 22 | +// docs.<domain> -> Notion public site, served under our hostname. |
| 23 | +export async function proxyNotion(request: Request, env: Env): Promise<Response> { |
| 24 | + const url = new URL(request.url); |
| 25 | + const path = url.pathname === "/" ? `/${env.NOTION_PAGE_ID}` : url.pathname; |
| 26 | + const upstream = new URL(path + url.search, NOTION_ORIGIN); |
| 27 | + |
| 28 | + const headers = new Headers(); |
| 29 | + for (const [k, v] of request.headers) { |
| 30 | + const lk = k.toLowerCase(); |
| 31 | + if (["host", "cookie", "cf-connecting-ip", "cf-ray", "cf-visitor", "x-forwarded-for", "x-forwarded-proto"].includes(lk)) continue; |
| 32 | + headers.set(k, v); |
| 33 | + } |
| 34 | + headers.set("host", "www.notion.so"); |
| 35 | + |
| 36 | + const upstreamResp = await fetch(upstream.toString(), { |
| 37 | + method: request.method === "POST" ? "GET" : request.method, |
| 38 | + headers, |
| 39 | + redirect: "follow", |
| 40 | + }); |
| 41 | + |
| 42 | + const outHeaders = new Headers(); |
| 43 | + for (const [k, v] of upstreamResp.headers) { |
| 44 | + if (DROP_RESPONSE_HEADERS.has(k.toLowerCase())) continue; |
| 45 | + outHeaders.set(k, v); |
| 46 | + } |
| 47 | + const loc = upstreamResp.headers.get("location"); |
| 48 | + if (loc) outHeaders.set("location", rewriteNotionUrl(loc, env.DOCS_HOSTNAME)); |
| 49 | + |
| 50 | + const ct = upstreamResp.headers.get("content-type") || ""; |
| 51 | + if (ct.includes("text/html")) { |
| 52 | + const html = await upstreamResp.text(); |
| 53 | + outHeaders.set("content-type", "text/html; charset=utf-8"); |
| 54 | + return new Response(rewriteHtml(html, env.DOCS_HOSTNAME), { |
| 55 | + status: upstreamResp.status, |
| 56 | + headers: outHeaders, |
| 57 | + }); |
| 58 | + } |
| 59 | + return new Response(upstreamResp.body, { status: upstreamResp.status, headers: outHeaders }); |
| 60 | +} |
| 61 | + |
| 62 | +function rewriteNotionUrl(value: string, docsHost: string): string { |
| 63 | + return value |
| 64 | + .replace(/https:\/\/(www\.)?notion\.so/gi, `https://${docsHost}`) |
| 65 | + .replace(/https:\/\/[a-z0-9-]+\.notion\.site/gi, `https://${docsHost}`); |
| 66 | +} |
| 67 | + |
| 68 | +function rewriteHtml(html: string, docsHost: string): string { |
| 69 | + return rewriteNotionUrl(html, docsHost).replace( |
| 70 | + /<head>/i, |
| 71 | + '<head><link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%23f6821f%22/><text x=%2250%22 y=%2268%22 font-size=%2260%22 text-anchor=%22middle%22 fill=%22white%22 font-family=%22sans-serif%22 font-weight=%22bold%22>D</text></svg>">', |
| 72 | + ); |
| 73 | +} |
| 74 | + |
| 75 | +// apex / www -> minimal DevArts Lab landing page (template). |
| 76 | +export function landingPage(env: Env): Response { |
| 77 | + const html = `<!doctype html> |
| 78 | +<html lang="en"> |
| 79 | +<head> |
| 80 | +<meta charset="utf-8"> |
| 81 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 82 | +<title>DevArts Lab</title> |
| 83 | +<meta name="description" content="DevArts Lab - independent engineering studio"> |
| 84 | +<style> |
| 85 | + :root { --bg:#0b0d10; --panel:#12151a; --border:#23272e; --text:#e6e8eb; --muted:#8b9199; --accent:#f6821f; } |
| 86 | + * { box-sizing:border-box; margin:0; } |
| 87 | + body { background:var(--bg); color:var(--text); font:15px/1.6 -apple-system,"Inter","Segoe UI",system-ui,sans-serif; |
| 88 | + min-height:100vh; display:flex; flex-direction:column; } |
| 89 | + header { padding:20px 32px; display:flex; align-items:center; gap:10px; border-bottom:1px solid var(--border); } |
| 90 | + .logo { width:28px; height:28px; border-radius:8px; background:var(--accent); color:#fff; display:grid; |
| 91 | + place-items:center; font-weight:700; } |
| 92 | + main { flex:1; display:grid; place-items:center; padding:48px 24px; } |
| 93 | + .hero { text-align:center; max-width:560px; } |
| 94 | + h1 { font-size:40px; font-weight:700; letter-spacing:-0.02em; } |
| 95 | + h1 em { color:var(--accent); font-style:normal; } |
| 96 | + p.sub { color:var(--muted); margin:12px 0 36px; font-size:16px; } |
| 97 | + .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; } |
| 98 | + .card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:20px; |
| 99 | + text-decoration:none; color:var(--text); transition:border-color .15s, transform .15s; } |
| 100 | + .card:hover { border-color:var(--accent); transform:translateY(-2px); } |
| 101 | + .card b { display:block; margin-bottom:4px; } |
| 102 | + .card span { color:var(--muted); font-size:13px; } |
| 103 | + footer { padding:18px 32px; border-top:1px solid var(--border); color:var(--muted); font-size:12px; |
| 104 | + display:flex; justify-content:space-between; } |
| 105 | + footer a { color:var(--muted); } |
| 106 | +</style> |
| 107 | +</head> |
| 108 | +<body> |
| 109 | +<header><div class="logo">D</div><b>DevArts Lab</b></header> |
| 110 | +<main> |
| 111 | + <div class="hero"> |
| 112 | + <h1>Dev<em>Arts</em> Lab</h1> |
| 113 | + <p class="sub">Independent engineering studio. Self-hosted mail, calendar, and docs on Cloudflare.</p> |
| 114 | + <div class="cards"> |
| 115 | + <a class="card" href="https://mail.${env.MAIL_DOMAIN}"><b>Mail</b><span>mail.${env.MAIL_DOMAIN}</span></a> |
| 116 | + <a class="card" href="https://${env.DOCS_HOSTNAME}"><b>Docs</b><span>${env.DOCS_HOSTNAME}</span></a> |
| 117 | + <a class="card" href="https://github.com/DevArtsLab"><b>GitHub</b><span>github.com/DevArtsLab</span></a> |
| 118 | + </div> |
| 119 | + </div> |
| 120 | +</main> |
| 121 | +<footer><span>© ${new Date().getUTCFullYear()} DevArts Lab</span><a href="mailto:contact@${env.MAIL_DOMAIN}">contact@${env.MAIL_DOMAIN}</a></footer> |
| 122 | +</body> |
| 123 | +</html>`; |
| 124 | + return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); |
| 125 | +} |
0 commit comments