From a8512ea9031f4bffb8b3cf76547f86813b93b72d Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Mon, 2 Mar 2026 12:26:14 -0500 Subject: [PATCH 1/3] Replace redirect flow with inline submit via Cloudflare Worker The previous flow took users off the page to a GitHub issue form. Now everything happens in the background: 1. User pastes URL, clicks Request 2. Frontend POSTs to /api/request (Cloudflare Worker) 3. Worker creates the GitHub issue (triggers auto-add pipeline) 4. User sees inline success message with their docs link Changes: - Add worker/ with Cloudflare Worker that proxies to GitHub Issues API - Rewrite frontend JS to use fetch() with loading/success/error states - Remove nav "Request a Repo" button (the inline form is the entry point) - Remove .request-btn CSS, add .submit-feedback state classes Worker deployment: cd worker && npx wrangler secret put GITHUB_TOKEN npx wrangler deploy Route repos.supermodeltools.com/api/* to the worker in Cloudflare. --- generate-index.go | 89 ++++++++++++++++++++++++++++-------------- worker/index.js | 93 ++++++++++++++++++++++++++++++++++++++++++++ worker/wrangler.toml | 7 ++++ 3 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 worker/index.js create mode 100644 worker/wrangler.toml diff --git a/generate-index.go b/generate-index.go index 0901df7..7e3f52c 100644 --- a/generate-index.go +++ b/generate-index.go @@ -196,18 +196,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b .site-nav { display: flex; gap: 16px; align-items: center; } .site-nav a { color: var(--text-muted); font-size: 14px; font-weight: 500; white-space: nowrap; } .site-nav a:hover { color: var(--text); text-decoration: none; } -.request-btn { - color: var(--accent-light) !important; - border: 1px solid var(--accent); - border-radius: var(--radius); - padding: 6px 12px; - transition: background 0.2s, color 0.2s; -} -.request-btn:hover { - background: var(--accent); - color: #fff !important; - text-decoration: none !important; -} .hero { padding: 64px 0 48px; text-align: center; @@ -395,14 +383,21 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b pointer-events: auto; } .submit-btn.active:hover { background: var(--accent-light); } -.submit-preview { - margin-top: 8px; +.submit-btn.loading { + opacity: 0.6; + pointer-events: none; +} +.submit-feedback { + margin-top: 10px; font-size: 13px; - color: var(--green); font-family: var(--mono); display: none; } -.submit-preview.visible { display: block; } +.submit-feedback.visible { display: block; } +.submit-feedback.preview { color: var(--text-muted); } +.submit-feedback.success { color: var(--green); } +.submit-feedback.success a { color: var(--green); text-decoration: underline; } +.submit-feedback.error { color: var(--red); } @media (max-width: 768px) { .container { padding: 0 16px; } .hero { padding: 40px 0 32px; } @@ -432,7 +427,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b Website GitHub X - + Request a Repo @@ -464,7 +458,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b -
+
@@ -510,10 +504,10 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b var noResults = document.getElementById('no-results'); var submitInput = document.getElementById('submit-url'); var submitBtn = document.getElementById('submit-btn'); - var submitPreview = document.getElementById('submit-preview'); + var feedback = document.getElementById('submit-feedback'); var noResultsRequest = document.getElementById('no-results-request'); - var issueBase = 'https://github.com/supermodeltools/supermodeltools.github.io/issues/new?template=request-repo.yml'; + var API_URL = '/api/request'; // --- Search --- searchInput.addEventListener('input', function() { @@ -546,36 +540,71 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b return null; } + function showFeedback(msg, type) { + feedback.className = 'submit-feedback visible ' + type; + feedback.innerHTML = msg; + } + submitInput.addEventListener('input', function() { var parsed = parseRepo(this.value); if (parsed) { var name = parsed.split('/')[1]; - submitPreview.textContent = '\u2192 Docs will be at repos.supermodeltools.com/' + name + '/'; - submitPreview.classList.add('visible'); + showFeedback('\u2192 repos.supermodeltools.com/' + name + '/', 'preview'); submitBtn.classList.add('active'); } else { - submitPreview.classList.remove('visible'); + feedback.className = 'submit-feedback'; submitBtn.classList.remove('active'); } }); - function submitRequest() { + async function submitRequest() { var parsed = parseRepo(submitInput.value); if (!parsed) return; + var repoUrl = 'https://github.com/' + parsed; var name = parsed.split('/')[1]; - var url = issueBase - + '&repo_url=' + encodeURIComponent(repoUrl) - + '&title=' + encodeURIComponent('[Repo Request] ' + name); - window.open(url, '_blank'); + + // Loading state + submitBtn.classList.add('loading'); + submitBtn.textContent = 'Submitting...'; + showFeedback('Setting up ' + name + '...', 'preview'); + + try { + var resp = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: repoUrl }), + }); + var data = await resp.json(); + + if (!resp.ok || !data.success) { + showFeedback(data.error || 'Something went wrong. Please try again.', 'error'); + submitBtn.classList.remove('loading'); + submitBtn.textContent = 'Request'; + return; + } + + // Success — show the link, clear the input + showFeedback( + '\u2713 Submitted! Docs will be generated at ' + + 'repos.supermodeltools.com/' + name + '/', 'success' + ); + submitInput.value = ''; + submitBtn.classList.remove('active', 'loading'); + submitBtn.textContent = 'Request'; + } catch (e) { + showFeedback('Network error. Please try again.', 'error'); + submitBtn.classList.remove('loading'); + submitBtn.textContent = 'Request'; + } } submitBtn.addEventListener('click', submitRequest); submitInput.addEventListener('keydown', function(e) { - if (e.key === 'Enter') submitRequest(); + if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest(); }); - // "No results" request link: pre-fill with search query as a guess + // "No results" link: scroll up and focus the submit input noResultsRequest.addEventListener('click', function() { var q = searchInput.value.trim(); submitInput.value = q; diff --git a/worker/index.js b/worker/index.js new file mode 100644 index 0000000..d6e90a9 --- /dev/null +++ b/worker/index.js @@ -0,0 +1,93 @@ +/** + * Cloudflare Worker — repo request proxy + * + * Receives a repo URL from the homepage, creates a GitHub issue + * (which triggers the auto-add-repo workflow), and returns immediately. + * + * Environment secrets (set via wrangler secret put): + * GITHUB_TOKEN — fine-grained PAT with issues:write on supermodeltools.github.io + * + * Deploy: + * cd worker && npx wrangler deploy + * + * Route (add in Cloudflare dashboard or wrangler.toml): + * repos.supermodeltools.com/api/* → this worker + */ + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': 'https://repos.supermodeltools.com', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +}; + +const REPO_RE = /github\.com\/([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+)/; + +export default { + async fetch(request, env) { + if (request.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + + if (request.method !== 'POST') { + return jsonResponse({ error: 'Method not allowed' }, 405); + } + + let body; + try { + body = await request.json(); + } catch { + return jsonResponse({ error: 'Invalid JSON' }, 400); + } + + const url = (body.url || '').trim().replace(/\/+$/, '').replace(/\.git$/, ''); + const match = url.match(REPO_RE); + if (!match) { + return jsonResponse({ error: 'Invalid GitHub repository URL' }, 400); + } + + const upstream = match[1]; + const name = upstream.split('/')[1]; + + // Create the GitHub issue — this triggers auto-add-repo.yml + const ghResponse = await fetch( + 'https://api.github.com/repos/supermodeltools/supermodeltools.github.io/issues', + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.GITHUB_TOKEN}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'supermodel-request-bot', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: `[Repo Request] ${name}`, + body: `### Repository URL\n\nhttps://github.com/${upstream}`, + labels: ['repo-request'], + }), + } + ); + + if (!ghResponse.ok) { + const err = await ghResponse.text(); + console.error('GitHub API error:', ghResponse.status, err); + return jsonResponse({ error: 'Failed to submit request. Please try again.' }, 502); + } + + const issue = await ghResponse.json(); + + return jsonResponse({ + success: true, + name, + upstream, + docs_url: `https://repos.supermodeltools.com/${name}/`, + issue_url: issue.html_url, + }); + }, +}; + +function jsonResponse(data, status = 200) { + return new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json', ...CORS_HEADERS }, + }); +} diff --git a/worker/wrangler.toml b/worker/wrangler.toml new file mode 100644 index 0000000..6576d33 --- /dev/null +++ b/worker/wrangler.toml @@ -0,0 +1,7 @@ +name = "repo-request" +main = "index.js" +compatibility_date = "2024-01-01" + +# Route: intercept /api/* on the custom domain +# Requires the domain to be proxied through Cloudflare +# routes = [{ pattern = "repos.supermodeltools.com/api/*", zone_name = "supermodeltools.com" }] From a4131c358420b3d8e109ad4ac583297ee2c0c5e3 Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Mon, 2 Mar 2026 12:33:34 -0500 Subject: [PATCH 2/3] Redirect to loading page after Generate, auto-refresh when docs ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX flow: 1. User pastes URL, clicks "Generate" (renamed from Request) 2. API fires in background, user is redirected to the docs URL 3. Since docs aren't deployed yet, GitHub Pages serves 404.html 4. 404.html detects the repo path and shows a loading page: - Supermodel eye logo with scanning animation - Animated progress steps (Forking → Analyzing → Building → Deploying) - Polls the URL every 5s for up to 10 minutes 5. When docs deploy, poll detects real content and page auto-reloads 6. User sees the full architecture docs For genuine 404s (not repo paths), shows a standard 404 page. Changes: - generate-index.go: add generate404() + notFoundTemplate - Rename "Request" → "Generate" in button text - Frontend JS redirects to docs_url after successful API call - 404.html: loading animation, step progress, polling logic --- generate-index.go | 331 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 318 insertions(+), 13 deletions(-) diff --git a/generate-index.go b/generate-index.go index 7e3f52c..d784bc0 100644 --- a/generate-index.go +++ b/generate-index.go @@ -59,6 +59,11 @@ func main() { os.Exit(1) } + if err := generate404(); err != nil { + fmt.Fprintf(os.Stderr, "Error generating 404.html: %v\n", err) + os.Exit(1) + } + // Copy CNAME and static root files to site directory if cname, err := os.ReadFile("CNAME"); err == nil { os.WriteFile("site/CNAME", cname, 0644) @@ -71,7 +76,11 @@ func main() { for _, cat := range cfg.Categories { totalRepos += len(cat.Repos) } - fmt.Printf("Generated index.html and sitemap.xml (%d repos)\n", totalRepos) + fmt.Printf("Generated site (%d repos)\n", totalRepos) +} + +func generate404() error { + return os.WriteFile("site/404.html", []byte(notFoundTemplate), 0644) } func generateSitemap(cfg Config) error { @@ -456,7 +465,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
Don't see your repo? Paste a URL to generate arch docs:
- +
@@ -566,7 +575,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b // Loading state submitBtn.classList.add('loading'); - submitBtn.textContent = 'Submitting...'; + submitBtn.textContent = 'Generating...'; showFeedback('Setting up ' + name + '...', 'preview'); try { @@ -580,22 +589,16 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b if (!resp.ok || !data.success) { showFeedback(data.error || 'Something went wrong. Please try again.', 'error'); submitBtn.classList.remove('loading'); - submitBtn.textContent = 'Request'; + submitBtn.textContent = 'Generate'; return; } - // Success — show the link, clear the input - showFeedback( - '\u2713 Submitted! Docs will be generated at ' + - 'repos.supermodeltools.com/' + name + '/', 'success' - ); - submitInput.value = ''; - submitBtn.classList.remove('active', 'loading'); - submitBtn.textContent = 'Request'; + // Redirect to the docs page — 404.html shows loading until docs are ready + window.location.href = data.docs_url; } catch (e) { showFeedback('Network error. Please try again.', 'error'); submitBtn.classList.remove('loading'); - submitBtn.textContent = 'Request'; + submitBtn.textContent = 'Generate'; } } @@ -617,3 +620,305 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b ` + +const notFoundTemplate = ` + + + + + Generating — Supermodel Architecture Docs + + + + + + + + + + + + + + + +` From 5af703ab86a78acae59408213ad4efe7de7e2cd0 Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Mon, 2 Mar 2026 12:38:25 -0500 Subject: [PATCH 3/3] =?UTF-8?q?Skeleton=20loading=20page=20instead=20of=20?= =?UTF-8?q?404=20=E2=80=94=20no=20404=20ever=20shown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After clicking Generate, the user lands on /generating/{name} which is served by the Cloudflare Worker as a real 200 page. It mirrors the actual arch-docs layout with shimmer placeholders: - Same header with repo name, nav links, "All Repos" back link - Skeleton hero with shimmer stat counters - Skeleton chart panels (Architecture Overview, Composition) - Skeleton taxonomy grids (Node Types, Domains, Languages) - Floating status bar: "Generating docs — analyzing codebase..." The status bar cycles through stages (forking, analyzing, building graphs, mapping architecture, deploying) on realistic timers. Polls the real docs URL every 5s. When docs are ready, the status bar flashes "Ready!" and redirects to the real page. The user sees what the page WILL look like, never sees a 404, and is seamlessly transitioned to the real docs when ready. Also: - Renamed "Request" → "Generate" everywhere - 404.html now just redirects to homepage (for genuine broken links) --- generate-index.go | 310 +----------------------------- worker/index.js | 447 +++++++++++++++++++++++++++++++++++++------ worker/wrangler.toml | 7 +- 3 files changed, 402 insertions(+), 362 deletions(-) diff --git a/generate-index.go b/generate-index.go index d784bc0..5492b64 100644 --- a/generate-index.go +++ b/generate-index.go @@ -80,7 +80,10 @@ func main() { } func generate404() error { - return os.WriteFile("site/404.html", []byte(notFoundTemplate), 0644) + return os.WriteFile("site/404.html", []byte(` +Redirecting… +

Redirecting to homepage

+`), 0644) } func generateSitemap(cfg Config) error { @@ -593,8 +596,8 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b return; } - // Redirect to the docs page — 404.html shows loading until docs are ready - window.location.href = data.docs_url; + // Redirect to the skeleton loading page — served by the worker + window.location.href = data.generating_url; } catch (e) { showFeedback('Network error. Please try again.', 'error'); submitBtn.classList.remove('loading'); @@ -621,304 +624,3 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b ` -const notFoundTemplate = ` - - - - - Generating — Supermodel Architecture Docs - - - - - - - - - - - - - - - -` diff --git a/worker/index.js b/worker/index.js index d6e90a9..030b807 100644 --- a/worker/index.js +++ b/worker/index.js @@ -1,17 +1,11 @@ /** - * Cloudflare Worker — repo request proxy + * Cloudflare Worker — repo request proxy + loading page * - * Receives a repo URL from the homepage, creates a GitHub issue - * (which triggers the auto-add-repo workflow), and returns immediately. + * POST /api/request — creates GitHub issue, returns docs URL + * GET /generating/* — serves skeleton loading page * * Environment secrets (set via wrangler secret put): * GITHUB_TOKEN — fine-grained PAT with issues:write on supermodeltools.github.io - * - * Deploy: - * cd worker && npx wrangler deploy - * - * Route (add in Cloudflare dashboard or wrangler.toml): - * repos.supermodeltools.com/api/* → this worker */ const CORS_HEADERS = { @@ -24,66 +18,84 @@ const REPO_RE = /github\.com\/([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+)/; export default { async fetch(request, env) { + const url = new URL(request.url); + + // CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: CORS_HEADERS }); } - if (request.method !== 'POST') { - return jsonResponse({ error: 'Method not allowed' }, 405); + // POST /api/request — create issue + if (url.pathname === '/api/request' && request.method === 'POST') { + return handleRequest(request, env); } - let body; - try { - body = await request.json(); - } catch { - return jsonResponse({ error: 'Invalid JSON' }, 400); + // GET /generating/{name} — serve loading page + const genMatch = url.pathname.match(/^\/generating\/([a-zA-Z0-9._-]+)/); + if (genMatch && request.method === 'GET') { + return serveSkeleton(genMatch[1]); } - const url = (body.url || '').trim().replace(/\/+$/, '').replace(/\.git$/, ''); - const match = url.match(REPO_RE); - if (!match) { - return jsonResponse({ error: 'Invalid GitHub repository URL' }, 400); - } + return new Response('Not found', { status: 404 }); + }, +}; - const upstream = match[1]; - const name = upstream.split('/')[1]; - - // Create the GitHub issue — this triggers auto-add-repo.yml - const ghResponse = await fetch( - 'https://api.github.com/repos/supermodeltools/supermodeltools.github.io/issues', - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.GITHUB_TOKEN}`, - Accept: 'application/vnd.github+json', - 'User-Agent': 'supermodel-request-bot', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - title: `[Repo Request] ${name}`, - body: `### Repository URL\n\nhttps://github.com/${upstream}`, - labels: ['repo-request'], - }), - } - ); +async function handleRequest(request, env) { + let body; + try { + body = await request.json(); + } catch { + return jsonResponse({ error: 'Invalid JSON' }, 400); + } + + const rawUrl = (body.url || '').trim().replace(/\/+$/, '').replace(/\.git$/, ''); + const match = rawUrl.match(REPO_RE); + if (!match) { + return jsonResponse({ error: 'Invalid GitHub repository URL' }, 400); + } + + const upstream = match[1]; + const name = upstream.split('/')[1]; - if (!ghResponse.ok) { - const err = await ghResponse.text(); - console.error('GitHub API error:', ghResponse.status, err); - return jsonResponse({ error: 'Failed to submit request. Please try again.' }, 502); + const ghResponse = await fetch( + 'https://api.github.com/repos/supermodeltools/supermodeltools.github.io/issues', + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.GITHUB_TOKEN}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'supermodel-request-bot', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: `[Repo Request] ${name}`, + body: `### Repository URL\n\nhttps://github.com/${upstream}`, + labels: ['repo-request'], + }), } + ); - const issue = await ghResponse.json(); + if (!ghResponse.ok) { + console.error('GitHub API error:', ghResponse.status, await ghResponse.text()); + return jsonResponse({ error: 'Failed to submit. Please try again.' }, 502); + } - return jsonResponse({ - success: true, - name, - upstream, - docs_url: `https://repos.supermodeltools.com/${name}/`, - issue_url: issue.html_url, - }); - }, -}; + return jsonResponse({ + success: true, + name, + upstream, + generating_url: `/generating/${name}`, + docs_url: `https://repos.supermodeltools.com/${name}/`, + }); +} + +function serveSkeleton(name) { + const html = SKELETON_HTML.replaceAll('{{NAME}}', escapeHtml(name)); + return new Response(html, { + status: 200, + headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }, + }); +} function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data), { @@ -91,3 +103,326 @@ function jsonResponse(data, status = 200) { headers: { 'Content-Type': 'application/json', ...CORS_HEADERS }, }); } + +function escapeHtml(s) { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +// --------------------------------------------------------------------------- +// Skeleton page — mirrors the real arch-docs layout with shimmer placeholders +// --------------------------------------------------------------------------- +const SKELETON_HTML = ` + + + + + {{NAME}} — Architecture Documentation + + + + + + + + + + +
+
+ + +
+

{{NAME}}

+
+ View on GitHub + Star + Fork +
+

Architecture documentation generated from code analysis. Explore every file, function, class, and domain.

+
+
Total Entities
+
Node Types
+
Languages
+
Domains
+
Subdomains
+
Top Directories
+
+
+ + +
+

Architecture Overview

+
+
+ + +
+

Codebase Composition

+
+
+ + +
+

Node Types

+
+
+
+
+
+
+
+
+
+ +
+

Domains

+
+
+
+
+
+
+
+ +
+

Languages

+
+
+
+
+
+
+
+
+ + + + +
+
+
Generating docs — analyzing codebase…
+
+ + + +`; diff --git a/worker/wrangler.toml b/worker/wrangler.toml index 6576d33..e3bac6d 100644 --- a/worker/wrangler.toml +++ b/worker/wrangler.toml @@ -2,6 +2,9 @@ name = "repo-request" main = "index.js" compatibility_date = "2024-01-01" -# Route: intercept /api/* on the custom domain +# Routes: intercept /api/* and /generating/* on the custom domain # Requires the domain to be proxied through Cloudflare -# routes = [{ pattern = "repos.supermodeltools.com/api/*", zone_name = "supermodeltools.com" }] +# routes = [ +# { pattern = "repos.supermodeltools.com/api/*", zone_name = "supermodeltools.com" }, +# { pattern = "repos.supermodeltools.com/generating/*", zone_name = "supermodeltools.com" } +# ]