11package main
22
33import (
4+ "encoding/json"
45 "fmt"
56 "html"
67 "net/url"
@@ -59,6 +60,11 @@ func main() {
5960 os .Exit (1 )
6061 }
6162
63+ if err := generate404 (cfg ); err != nil {
64+ fmt .Fprintf (os .Stderr , "Error generating 404: %v\n " , err )
65+ os .Exit (1 )
66+ }
67+
6268 // Copy CNAME and static root files to site directory
6369 if cname , err := os .ReadFile ("CNAME" ); err == nil {
6470 os .WriteFile ("site/CNAME" , cname , 0644 )
@@ -71,7 +77,7 @@ func main() {
7177 for _ , cat := range cfg .Categories {
7278 totalRepos += len (cat .Repos )
7379 }
74- fmt .Printf ("Generated index.html and sitemap.xml (%d repos)\n " , totalRepos )
80+ fmt .Printf ("Generated index.html, sitemap.xml, and 404.html (%d repos)\n " , totalRepos )
7581}
7682
7783func generateSitemap (cfg Config ) error {
@@ -124,6 +130,34 @@ func generateIndex(cfg Config) error {
124130 return tmpl .Execute (f , cfg )
125131}
126132
133+ func generate404 (cfg Config ) error {
134+ var repoNames []string
135+ for _ , cat := range cfg .Categories {
136+ for _ , repo := range cat .Repos {
137+ repoNames = append (repoNames , repo .Name )
138+ }
139+ }
140+ repoNamesJSON , err := json .Marshal (repoNames )
141+ if err != nil {
142+ return fmt .Errorf ("marshaling repo names: %w" , err )
143+ }
144+
145+ tmpl , err := template .New ("404" ).Parse (notFoundTemplate )
146+ if err != nil {
147+ return fmt .Errorf ("parsing 404 template: %w" , err )
148+ }
149+
150+ f , err := os .Create ("site/404.html" )
151+ if err != nil {
152+ return fmt .Errorf ("creating 404.html: %w" , err )
153+ }
154+ defer f .Close ()
155+
156+ return tmpl .Execute (f , map [string ]interface {}{
157+ "RepoNamesJSON" : string (repoNamesJSON ),
158+ })
159+ }
160+
127161const indexTemplate = `<!DOCTYPE html>
128162<html lang="en">
129163<head>
@@ -450,3 +484,251 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
450484</body>
451485</html>
452486`
487+
488+ const notFoundTemplate = `<!DOCTYPE html>
489+ <html lang="en">
490+ <head>
491+ <meta charset="utf-8">
492+ <meta name="viewport" content="width=device-width, initial-scale=1">
493+ <title id="page-title">Supermodel Tools</title>
494+ <meta name="robots" content="noindex">
495+ <link rel="preconnect" href="https://fonts.googleapis.com">
496+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
497+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
498+ <style>
499+ :root {
500+ --bg: #0f1117;
501+ --bg-card: #1a1d27;
502+ --border: #2a2e3e;
503+ --text: #e4e4e7;
504+ --text-muted: #9ca3af;
505+ --accent: #6366f1;
506+ --accent-light: #818cf8;
507+ --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
508+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
509+ --max-w: 1200px;
510+ --radius: 8px;
511+ }
512+ * { margin: 0; padding: 0; box-sizing: border-box; }
513+ html { overflow-x: hidden; }
514+ body {
515+ font-family: var(--font);
516+ background: var(--bg);
517+ color: var(--text);
518+ line-height: 1.6;
519+ -webkit-font-smoothing: antialiased;
520+ min-height: 100vh;
521+ display: flex;
522+ flex-direction: column;
523+ }
524+ a { color: var(--accent-light); text-decoration: none; }
525+ a:hover { text-decoration: underline; }
526+ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; border-radius: 2px; }
527+ .container { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; }
528+ .site-header {
529+ border-bottom: 1px solid var(--border);
530+ padding: 16px 0;
531+ }
532+ .site-header .container {
533+ display: flex;
534+ align-items: center;
535+ justify-content: space-between;
536+ gap: 16px;
537+ }
538+ .site-brand {
539+ font-size: 18px;
540+ font-weight: 700;
541+ color: var(--text);
542+ display: flex;
543+ align-items: center;
544+ gap: 8px;
545+ white-space: nowrap;
546+ flex-shrink: 0;
547+ }
548+ .site-brand:hover { text-decoration: none; color: var(--accent-light); }
549+ .site-brand svg { width: 24px; height: 24px; }
550+ .site-nav { display: flex; gap: 16px; align-items: center; }
551+ .site-nav a { color: var(--text-muted); font-size: 14px; font-weight: 500; white-space: nowrap; }
552+ .site-nav a:hover { color: var(--text); text-decoration: none; }
553+ main { flex: 1; display: flex; align-items: center; }
554+ .not-found {
555+ padding: 80px 0;
556+ text-align: center;
557+ max-width: 560px;
558+ margin: 0 auto;
559+ display: none;
560+ }
561+ .not-found-icon {
562+ width: 56px;
563+ height: 56px;
564+ margin: 0 auto 24px;
565+ color: var(--border);
566+ }
567+ .not-found h1 {
568+ font-size: 28px;
569+ font-weight: 700;
570+ margin-bottom: 12px;
571+ line-height: 1.3;
572+ }
573+ .repo-name {
574+ font-family: var(--mono);
575+ color: var(--accent-light);
576+ }
577+ .not-found p {
578+ color: var(--text-muted);
579+ font-size: 16px;
580+ margin-bottom: 32px;
581+ max-width: 440px;
582+ margin-left: auto;
583+ margin-right: auto;
584+ }
585+ .not-found-actions {
586+ display: flex;
587+ gap: 12px;
588+ justify-content: center;
589+ flex-wrap: wrap;
590+ }
591+ .btn-primary {
592+ display: inline-flex;
593+ align-items: center;
594+ gap: 8px;
595+ padding: 10px 20px;
596+ background: var(--accent);
597+ color: #fff;
598+ border-radius: var(--radius);
599+ font-size: 14px;
600+ font-weight: 600;
601+ transition: background 0.2s;
602+ }
603+ .btn-primary:hover { background: var(--accent-light); text-decoration: none; color: #fff; }
604+ .btn-secondary {
605+ display: inline-flex;
606+ align-items: center;
607+ gap: 8px;
608+ padding: 10px 20px;
609+ background: transparent;
610+ color: var(--text-muted);
611+ border: 1px solid var(--border);
612+ border-radius: var(--radius);
613+ font-size: 14px;
614+ font-weight: 600;
615+ transition: border-color 0.2s, color 0.2s;
616+ }
617+ .btn-secondary:hover { border-color: var(--accent); color: var(--text); text-decoration: none; }
618+ .site-footer {
619+ border-top: 1px solid var(--border);
620+ padding: 32px 0;
621+ color: var(--text-muted);
622+ font-size: 13px;
623+ text-align: center;
624+ }
625+ @media (max-width: 768px) {
626+ .container { padding: 0 16px; }
627+ .not-found { padding: 48px 0; }
628+ .not-found h1 { font-size: 22px; }
629+ .not-found p { font-size: 14px; }
630+ }
631+ </style>
632+ </head>
633+ <body>
634+ <header class="site-header">
635+ <div class="container">
636+ <a href="/" class="site-brand">
637+ <svg viewBox="0 0 90 78" fill="none" xmlns="http://www.w3.org/2000/svg">
638+ <path d="M90 61.1124C75.9375 73.4694 59.8419 78 44.7554 78C29.669 78 11.8614 72.6122 0 61.1011V16.9458C11.6168 6 29.891 0 44.9887 0C62.77 0 78.8723 6.97959 89.9887 16.9458V61.1124H90ZM88.1881 38.9553C77.7923 22.8824 59.8983 15.7959 44.7554 15.7959C29.6126 15.7959 13.4515 21.9008 1.556 38.9444C12.5382 54.69 26.9 62.5085 44.7554 62.0944C67.6297 61.5639 77.6495 51.9184 88.1881 38.9553ZM44.7554 16.3475C32.4756 16.3475 22.3888 26.6879 22.2554 38.9388C34.3765 38.9162 44.7554 29.1429 44.7554 16.3475C44.7554 29.1429 55.1344 38.9162 67.2554 38.9388C67.1202 26.5216 57.1141 16.3475 44.7554 16.3475ZM44.7554 61.5639C44.7554 48.4898 34.3765 38.9613 22.2554 38.9388C22.3888 51.1897 32.4756 61.5639 44.7554 61.5639C57.0352 61.5639 67.122 51.1897 67.2554 38.9388C55.1344 38.9613 44.7554 48.4898 44.7554 61.5639Z" fill="currentColor"/>
639+ </svg>
640+ Supermodel Tools
641+ </a>
642+ <nav class="site-nav">
643+ <a href="https://supermodeltools.com">Website</a>
644+ <a href="https://github.com/supermodeltools">GitHub</a>
645+ <a href="https://x.com/supermodeltools">X</a>
646+ </nav>
647+ </div>
648+ </header>
649+
650+ <main>
651+ <div class="container">
652+ <div id="not-found" class="not-found">
653+ <svg class="not-found-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
654+ <circle cx="11" cy="11" r="8"/>
655+ <path d="m21 21-4.3-4.3"/>
656+ <path d="M11 8v3M11 15h.01"/>
657+ </svg>
658+ <h1 id="not-found-title"></h1>
659+ <p id="not-found-desc"></p>
660+ <div id="not-found-actions" class="not-found-actions"></div>
661+ </div>
662+ </div>
663+ </main>
664+
665+ <footer class="site-footer">
666+ <div class="container">
667+ <p>Generated with <a href="https://github.com/supermodeltools/arch-docs">arch-docs</a> by <a href="https://supermodeltools.com">supermodeltools</a></p>
668+ </div>
669+ </footer>
670+
671+ <script>
672+ (function() {
673+ var KNOWN_REPOS = {{.RepoNamesJSON}};
674+
675+ // Extract and sanitize the first path segment
676+ var seg = window.location.pathname.split('/').filter(function(s) { return s.length > 0; })[0] || '';
677+ // Allow only safe repo name characters to prevent XSS
678+ var repo = seg.replace(/[^a-zA-Z0-9\-._]/g, '');
679+
680+ if (repo && KNOWN_REPOS.indexOf(repo) !== -1) {
681+ // Known repo: redirect to its docs directory
682+ window.location.replace('/' + repo + '/');
683+ return;
684+ }
685+
686+ // Unknown repo or bare 404: show landing page
687+ var titleEl = document.getElementById('page-title');
688+ var h1 = document.getElementById('not-found-title');
689+ var desc = document.getElementById('not-found-desc');
690+ var actions = document.getElementById('not-found-actions');
691+ var container = document.getElementById('not-found');
692+
693+ if (repo) {
694+ titleEl.textContent = repo + ' \u2014 Supermodel Tools';
695+
696+ h1.appendChild(document.createTextNode('No docs found for '));
697+ var repoSpan = document.createElement('span');
698+ repoSpan.className = 'repo-name';
699+ repoSpan.textContent = repo;
700+ h1.appendChild(repoSpan);
701+
702+ desc.textContent = "We haven\u2019t generated architecture docs for this repo yet. Request it below and we\u2019ll get it added!";
703+
704+ var issueTitle = '[Repo Request] ' + repo;
705+ var issueBody = 'Please add architecture documentation for **' + repo + '**.\n\nRequested via https://repos.supermodeltools.com/' + repo;
706+ var issueURL = 'https://github.com/supermodeltools/supermodeltools.github.io/issues/new' +
707+ '?title=' + encodeURIComponent(issueTitle) +
708+ '&body=' + encodeURIComponent(issueBody);
709+
710+ var genBtn = document.createElement('a');
711+ genBtn.href = issueURL;
712+ genBtn.className = 'btn-primary';
713+ genBtn.target = '_blank';
714+ genBtn.rel = 'noopener noreferrer';
715+ genBtn.textContent = 'Generate Docs';
716+ actions.appendChild(genBtn);
717+ } else {
718+ titleEl.textContent = '404 \u2014 Supermodel Tools';
719+ h1.textContent = 'Page not found';
720+ desc.textContent = "The page you\u2019re looking for doesn\u2019t exist.";
721+ }
722+
723+ var homeBtn = document.createElement('a');
724+ homeBtn.href = '/';
725+ homeBtn.className = 'btn-secondary';
726+ homeBtn.textContent = 'Browse all repos';
727+ actions.appendChild(homeBtn);
728+
729+ container.style.display = '';
730+ })();
731+ </script>
732+ </body>
733+ </html>
734+ `
0 commit comments