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 {
@@ -450,3 +456,269 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
450456</body>
451457</html>
452458`
459+
460+ func generate404 (cfg Config ) error {
461+ var repoNames []string
462+ for _ , cat := range cfg .Categories {
463+ for _ , repo := range cat .Repos {
464+ repoNames = append (repoNames , repo .Name )
465+ }
466+ }
467+ repoNamesJSON , err := json .Marshal (repoNames )
468+ if err != nil {
469+ return fmt .Errorf ("marshaling repo names: %w" , err )
470+ }
471+
472+ type data404 struct {
473+ RepoNamesJSON string
474+ }
475+
476+ tmpl , err := template .New ("404" ).Parse (notFoundTemplate )
477+ if err != nil {
478+ return fmt .Errorf ("parsing 404 template: %w" , err )
479+ }
480+
481+ f , err := os .Create ("site/404.html" )
482+ if err != nil {
483+ return fmt .Errorf ("creating 404.html: %w" , err )
484+ }
485+ defer f .Close ()
486+
487+ return tmpl .Execute (f , data404 {RepoNamesJSON : string (repoNamesJSON )})
488+ }
489+
490+ const notFoundTemplate = `<!DOCTYPE html>
491+ <html lang="en">
492+ <head>
493+ <meta charset="utf-8">
494+ <meta name="viewport" content="width=device-width, initial-scale=1">
495+ <title>Supermodel Tools — Not Found</title>
496+ <meta name="description" content="Architecture documentation for popular open source repositories.">
497+ <link rel="preconnect" href="https://fonts.googleapis.com">
498+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
499+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
500+ <style>
501+ :root {
502+ --bg: #0f1117;
503+ --bg-card: #1a1d27;
504+ --border: #2a2e3e;
505+ --text: #e4e4e7;
506+ --text-muted: #9ca3af;
507+ --accent: #6366f1;
508+ --accent-light: #818cf8;
509+ --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
510+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
511+ --max-w: 1200px;
512+ --radius: 8px;
513+ }
514+ * { margin: 0; padding: 0; box-sizing: border-box; }
515+ html { overflow-x: hidden; }
516+ body {
517+ font-family: var(--font);
518+ background: var(--bg);
519+ color: var(--text);
520+ line-height: 1.6;
521+ -webkit-font-smoothing: antialiased;
522+ min-height: 100vh;
523+ display: flex;
524+ flex-direction: column;
525+ }
526+ a { color: var(--accent-light); text-decoration: none; }
527+ a:hover { text-decoration: underline; }
528+ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; border-radius: 2px; }
529+ .container { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; }
530+ .site-header {
531+ border-bottom: 1px solid var(--border);
532+ padding: 16px 0;
533+ background: var(--bg);
534+ }
535+ .site-header .container {
536+ display: flex;
537+ align-items: center;
538+ justify-content: space-between;
539+ gap: 16px;
540+ }
541+ .site-brand {
542+ font-size: 18px;
543+ font-weight: 700;
544+ color: var(--text);
545+ display: flex;
546+ align-items: center;
547+ gap: 8px;
548+ white-space: nowrap;
549+ flex-shrink: 0;
550+ }
551+ .site-brand:hover { text-decoration: none; color: var(--accent-light); }
552+ .site-brand svg { width: 24px; height: 24px; }
553+ .site-nav { display: flex; gap: 16px; align-items: center; }
554+ .site-nav a { color: var(--text-muted); font-size: 14px; font-weight: 500; white-space: nowrap; }
555+ .site-nav a:hover { color: var(--text); text-decoration: none; }
556+ main {
557+ flex: 1;
558+ display: flex;
559+ align-items: center;
560+ justify-content: center;
561+ }
562+ .generate-hero {
563+ text-align: center;
564+ padding: 64px 24px;
565+ max-width: 560px;
566+ margin: 0 auto;
567+ }
568+ .generate-icon {
569+ width: 64px;
570+ height: 64px;
571+ margin: 0 auto 24px;
572+ color: var(--accent-light);
573+ opacity: 0.8;
574+ }
575+ .generate-hero h1 {
576+ font-size: 28px;
577+ font-weight: 700;
578+ margin-bottom: 12px;
579+ }
580+ .repo-slug {
581+ color: var(--accent-light);
582+ font-family: var(--mono);
583+ }
584+ .generate-hero p {
585+ color: var(--text-muted);
586+ font-size: 16px;
587+ margin-bottom: 32px;
588+ line-height: 1.7;
589+ }
590+ .btn-generate {
591+ display: inline-flex;
592+ align-items: center;
593+ gap: 8px;
594+ padding: 12px 28px;
595+ background: var(--accent);
596+ color: #fff;
597+ border-radius: var(--radius);
598+ font-size: 15px;
599+ font-weight: 600;
600+ font-family: var(--font);
601+ text-decoration: none;
602+ transition: background 0.2s, transform 0.1s;
603+ }
604+ .btn-generate:hover {
605+ background: var(--accent-light);
606+ color: #fff;
607+ text-decoration: none;
608+ transform: translateY(-1px);
609+ }
610+ .btn-generate svg { width: 18px; height: 18px; }
611+ .browse-link {
612+ display: block;
613+ margin-top: 20px;
614+ font-size: 14px;
615+ color: var(--text-muted);
616+ }
617+ .site-footer {
618+ border-top: 1px solid var(--border);
619+ padding: 32px 0;
620+ color: var(--text-muted);
621+ font-size: 13px;
622+ text-align: center;
623+ }
624+ #content { display: none; width: 100%; }
625+ @media (max-width: 768px) {
626+ .container { padding: 0 16px; }
627+ .generate-hero { padding: 48px 0; }
628+ .generate-hero h1 { font-size: 22px; }
629+ .generate-hero p { font-size: 15px; }
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 id="content">
652+ <div class="generate-hero">
653+ <svg class="generate-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
654+ <path d="M12 2L2 7l10 5 10-5-10-5z"/>
655+ <path d="M2 17l10 5 10-5"/>
656+ <path d="M2 12l10 5 10-5"/>
657+ </svg>
658+ <h1 id="page-title">No docs yet for <span class="repo-slug" id="repo-name"></span></h1>
659+ <p id="page-desc">Architecture docs for this repository haven't been generated yet.<br>Request them and we'll add it to the index.</p>
660+ <a href="#" id="generate-btn" class="btn-generate" style="display:none" target="_blank" rel="noopener noreferrer">
661+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
662+ Generate Docs
663+ </a>
664+ <p class="browse-link"><a href="/">Browse existing docs</a></p>
665+ </div>
666+ </div>
667+ </main>
668+
669+ <footer class="site-footer">
670+ <div class="container">
671+ <p>Generated with <a href="https://github.com/supermodeltools/arch-docs">arch-docs</a> by <a href="https://supermodeltools.com">supermodeltools</a></p>
672+ </div>
673+ </footer>
674+
675+ <noscript>
676+ <style>#content { display: block !important; }</style>
677+ </noscript>
678+
679+ <script>
680+ (function() {
681+ var knownRepos = {{.RepoNamesJSON}};
682+
683+ // Extract first path segment from the current URL
684+ var rawPath = window.location.pathname;
685+ var slug = rawPath.replace(/^\/+/, '').split('/')[0] || '';
686+
687+ // If the slug is a known repo, redirect to its docs
688+ if (slug && knownRepos.indexOf(slug) !== -1) {
689+ window.location.replace('/' + slug + '/');
690+ return;
691+ }
692+
693+ // Show the landing page
694+ var content = document.getElementById('content');
695+ if (content) content.style.display = 'block';
696+
697+ var titleEl = document.getElementById('page-title');
698+ var nameEl = document.getElementById('repo-name');
699+ var descEl = document.getElementById('page-desc');
700+
701+ if (slug) {
702+ // Safely set repo name using textContent (XSS-safe — no innerHTML)
703+ if (nameEl) nameEl.textContent = slug;
704+
705+ // Configure the Generate Docs button
706+ var btn = document.getElementById('generate-btn');
707+ if (btn) {
708+ var title = '[Repo Request] ' + slug;
709+ var body = 'Please generate architecture docs for the \u0060' + slug + '\u0060 repository.\n\n@claude';
710+ btn.href = 'https://github.com/supermodeltools/supermodeltools.github.io/issues/new'
711+ + '?title=' + encodeURIComponent(title)
712+ + '&body=' + encodeURIComponent(body);
713+ btn.style.display = 'inline-flex';
714+ }
715+ } else {
716+ // No slug — generic not-found message
717+ if (titleEl) titleEl.textContent = 'Page not found';
718+ if (descEl) descEl.textContent = 'The page you\u2019re looking for doesn\u2019t exist.';
719+ }
720+ })();
721+ </script>
722+ </body>
723+ </html>
724+ `
0 commit comments