@knowcode/doc-builder 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +47 -7
  4. package/RELEASE-NOTES-1.7.5.md +64 -0
  5. package/add-user-clive.sql +35 -0
  6. package/add-user-lindsay-fixed.sql +85 -0
  7. package/add-user-lindsay.sql +68 -0
  8. package/add-user-pmorgan.sql +35 -0
  9. package/add-user-robbie.sql +35 -0
  10. package/add-wru-users.sql +105 -0
  11. package/cli.js +223 -8
  12. package/grant-access.sql +15 -0
  13. package/html/README.html +14 -4
  14. package/html/auth.js +97 -0
  15. package/html/documentation-index.html +14 -4
  16. package/html/guides/authentication-default-change.html +302 -0
  17. package/html/guides/authentication-guide.html +32 -14
  18. package/html/guides/cache-control-anti-pattern.html +361 -0
  19. package/html/guides/claude-workflow-guide.html +14 -4
  20. package/html/guides/documentation-standards.html +14 -4
  21. package/html/guides/next-steps-walkthrough.html +638 -0
  22. package/html/guides/phosphor-icons-guide.html +14 -4
  23. package/html/guides/public-site-deployment.html +363 -0
  24. package/html/guides/search-engine-verification-guide.html +14 -4
  25. package/html/guides/seo-guide.html +14 -4
  26. package/html/guides/seo-optimization-guide.html +14 -4
  27. package/html/guides/supabase-auth-implementation-plan.html +543 -0
  28. package/html/guides/supabase-auth-integration-plan.html +671 -0
  29. package/html/guides/supabase-auth-setup-guide.html +498 -0
  30. package/html/guides/troubleshooting-guide.html +14 -4
  31. package/html/guides/vercel-deployment-auth-setup.html +337 -0
  32. package/html/guides/windows-setup-guide.html +14 -4
  33. package/html/index.html +14 -4
  34. package/html/launch/README.html +14 -4
  35. package/html/launch/bubble-plugin-specification.html +14 -4
  36. package/html/launch/go-to-market-strategy.html +14 -4
  37. package/html/launch/launch-announcements.html +14 -4
  38. package/html/login.html +102 -0
  39. package/html/logout.html +18 -0
  40. package/html/sitemap.xml +69 -21
  41. package/html/vercel-cli-setup-guide.html +14 -4
  42. package/html/vercel-first-time-setup-guide.html +14 -4
  43. package/lib/config.js +33 -29
  44. package/lib/core-builder.js +142 -88
  45. package/lib/supabase-auth.js +295 -0
  46. package/manage-users.sql +191 -0
  47. package/package.json +2 -1
  48. package/public-config.js +22 -0
  49. package/public-html/404.html +115 -0
  50. package/public-html/README.html +149 -0
  51. package/public-html/css/notion-style.css +2036 -0
  52. package/public-html/index.html +149 -0
  53. package/public-html/js/main.js +1485 -0
  54. package/quick-test-commands.md +40 -0
  55. package/recordings/Screenshot 2025-07-24 at 18.22.01.png +0 -0
  56. package/setup-database.sql +41 -0
  57. package/test-auth-config.js +17 -0
  58. package/test-docs/README.md +39 -0
  59. package/test-html/404.html +115 -0
  60. package/test-html/README.html +172 -0
  61. package/test-html/auth.js +97 -0
  62. package/test-html/css/notion-style.css +2036 -0
  63. package/test-html/index.html +172 -0
  64. package/test-html/js/auth.js +97 -0
  65. package/test-html/js/main.js +1485 -0
  66. package/test-html/login.html +102 -0
  67. package/test-html/logout.html +18 -0
  68. package/update-domain.sql +9 -0
  69. package/view-all-users.sql +40 -0
  70. package/wru-auth-config.js +17 -0
  71. /package/{assets → public-html}/js/auth.js +0 -0
@@ -0,0 +1,295 @@
1
+ const { createClient } = require('@supabase/supabase-js');
2
+
3
+ /**
4
+ * Supabase Authentication Module for @knowcode/doc-builder
5
+ *
6
+ * This module provides secure authentication functionality using Supabase's
7
+ * built-in auth system. It replaces the insecure basic auth implementation.
8
+ */
9
+
10
+ class SupabaseAuth {
11
+ constructor(config) {
12
+ if (!config.supabaseUrl || !config.supabaseAnonKey) {
13
+ throw new Error('Supabase URL and anonymous key are required for authentication');
14
+ }
15
+
16
+ this.config = config;
17
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
18
+ auth: {
19
+ persistSession: true,
20
+ autoRefreshToken: true,
21
+ detectSessionInUrl: true
22
+ }
23
+ });
24
+
25
+ this.siteId = config.siteId;
26
+ }
27
+
28
+ /**
29
+ * Generate client-side auth script for inclusion in HTML pages
30
+ */
31
+ generateAuthScript() {
32
+ return `
33
+ /**
34
+ * Supabase Authentication for Documentation Site
35
+ * Generated by @knowcode/doc-builder
36
+ */
37
+
38
+ (function() {
39
+ 'use strict';
40
+
41
+ // Skip auth check on login and logout pages
42
+ const currentPage = window.location.pathname;
43
+ if (currentPage === '/login.html' || currentPage === '/logout.html' ||
44
+ currentPage.includes('login') || currentPage.includes('logout')) {
45
+ return;
46
+ }
47
+
48
+ // Initialize Supabase client
49
+ const { createClient } = supabase;
50
+ const supabaseClient = createClient('${this.config.supabaseUrl}', '${this.config.supabaseAnonKey}', {
51
+ auth: {
52
+ persistSession: true,
53
+ autoRefreshToken: true,
54
+ detectSessionInUrl: true
55
+ }
56
+ });
57
+
58
+ // Check authentication and site access
59
+ async function checkAuth() {
60
+ try {
61
+ // Get current user session
62
+ const { data: { user }, error: userError } = await supabaseClient.auth.getUser();
63
+
64
+ if (userError || !user) {
65
+ redirectToLogin();
66
+ return;
67
+ }
68
+
69
+ // Check if user has access to this site
70
+ const { data: access, error: accessError } = await supabaseClient
71
+ .from('docbuilder_access')
72
+ .select('*')
73
+ .eq('user_id', user.id)
74
+ .eq('site_id', '${this.config.siteId}')
75
+ .single();
76
+
77
+ if (accessError || !access) {
78
+ showAccessDenied();
79
+ return;
80
+ }
81
+
82
+ // User is authenticated and has access
83
+ console.log('User authenticated and authorized');
84
+ document.body.classList.add('authenticated');
85
+
86
+ } catch (error) {
87
+ console.error('Auth check failed:', error);
88
+ redirectToLogin();
89
+ }
90
+ }
91
+
92
+ // Redirect to login page
93
+ function redirectToLogin() {
94
+ const currentUrl = window.location.pathname + window.location.search;
95
+ const loginUrl = '/login.html' + (currentUrl !== '/' ? '?redirect=' + encodeURIComponent(currentUrl) : '');
96
+ window.location.href = loginUrl;
97
+ }
98
+
99
+ // Show access denied message
100
+ function showAccessDenied() {
101
+ document.body.classList.add('authenticated'); // Show the body
102
+ document.body.innerHTML = \`
103
+ <div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: Inter, sans-serif;">
104
+ <div style="text-align: center; max-width: 400px;">
105
+ <h1 style="color: #ef4444; margin-bottom: 1rem;">Access Denied</h1>
106
+ <p style="color: #6b7280; margin-bottom: 2rem;">You don't have permission to view this documentation site.</p>
107
+ <a href="/login.html" style="background: #3b82f6; color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none;">Try Different Account</a>
108
+ </div>
109
+ </div>
110
+ \`;
111
+ }
112
+
113
+ // Add logout functionality
114
+ document.addEventListener('DOMContentLoaded', function() {
115
+ const logoutLinks = document.querySelectorAll('a[href*="logout"]');
116
+ logoutLinks.forEach(link => {
117
+ link.addEventListener('click', async function(e) {
118
+ e.preventDefault();
119
+ await supabaseClient.auth.signOut();
120
+ window.location.href = '/logout.html';
121
+ });
122
+ });
123
+ });
124
+
125
+ // Run auth check
126
+ checkAuth();
127
+
128
+ })();
129
+ `;
130
+ }
131
+
132
+ /**
133
+ * Generate login page HTML
134
+ */
135
+ generateLoginPage(config) {
136
+ const siteName = config.siteName || 'Documentation';
137
+
138
+ return `<!DOCTYPE html>
139
+ <html lang="en">
140
+ <head>
141
+ <meta charset="UTF-8">
142
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
+ <title>Login - ${siteName}</title>
144
+ <link rel="stylesheet" href="css/notion-style.css">
145
+ <script src="https://unpkg.com/@supabase/supabase-js@2"></script>
146
+ </head>
147
+ <body class="auth-page">
148
+ <div class="auth-container">
149
+ <div class="auth-box">
150
+ <h1>Login to ${siteName}</h1>
151
+ <form id="login-form">
152
+ <div class="form-group">
153
+ <label for="email">Email</label>
154
+ <input type="email" id="email" name="email" required>
155
+ </div>
156
+ <div class="form-group">
157
+ <label for="password">Password</label>
158
+ <input type="password" id="password" name="password" required>
159
+ </div>
160
+ <button type="submit" class="auth-button">Login</button>
161
+ </form>
162
+ <div id="error-message" class="error-message"></div>
163
+ <div class="auth-links">
164
+ <a href="#" id="forgot-password">Forgot Password?</a>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <script>
170
+ // Initialize Supabase
171
+ const { createClient } = supabase;
172
+ const supabaseClient = createClient('${this.config.supabaseUrl}', '${this.config.supabaseAnonKey}');
173
+
174
+ // Handle login form
175
+ document.getElementById('login-form').addEventListener('submit', async function(e) {
176
+ e.preventDefault();
177
+
178
+ const email = document.getElementById('email').value;
179
+ const password = document.getElementById('password').value;
180
+ const errorDiv = document.getElementById('error-message');
181
+
182
+ try {
183
+ // Sign in with Supabase
184
+ const { data, error } = await supabaseClient.auth.signInWithPassword({
185
+ email: email,
186
+ password: password
187
+ });
188
+
189
+ if (error) throw error;
190
+
191
+ // Check if user has access to this site
192
+ const { data: access, error: accessError } = await supabaseClient
193
+ .from('docbuilder_access')
194
+ .select('*')
195
+ .eq('user_id', data.user.id)
196
+ .eq('site_id', '${this.config.siteId}')
197
+ .single();
198
+
199
+ if (accessError || !access) {
200
+ await supabaseClient.auth.signOut();
201
+ throw new Error('You do not have access to this documentation site');
202
+ }
203
+
204
+ // Redirect to requested page
205
+ const params = new URLSearchParams(window.location.search);
206
+ const redirect = params.get('redirect') || '/';
207
+ window.location.href = redirect;
208
+
209
+ } catch (error) {
210
+ errorDiv.textContent = error.message;
211
+ errorDiv.style.display = 'block';
212
+ }
213
+ });
214
+
215
+ // Handle forgot password
216
+ document.getElementById('forgot-password').addEventListener('click', async function(e) {
217
+ e.preventDefault();
218
+
219
+ const email = document.getElementById('email').value;
220
+ if (!email) {
221
+ alert('Please enter your email address first');
222
+ return;
223
+ }
224
+
225
+ try {
226
+ const { error } = await supabaseClient.auth.resetPasswordForEmail(email, {
227
+ redirectTo: window.location.origin + '/login.html'
228
+ });
229
+
230
+ if (error) throw error;
231
+
232
+ alert('Password reset email sent! Check your inbox.');
233
+ } catch (error) {
234
+ alert('Error sending reset email: ' + error.message);
235
+ }
236
+ });
237
+ </script>
238
+ </body>
239
+ </html>`;
240
+ }
241
+
242
+ /**
243
+ * Generate logout page HTML
244
+ */
245
+ generateLogoutPage(config) {
246
+ const siteName = config.siteName || 'Documentation';
247
+
248
+ return `<!DOCTYPE html>
249
+ <html lang="en">
250
+ <head>
251
+ <meta charset="UTF-8">
252
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
253
+ <title>Logged Out - ${siteName}</title>
254
+ <link rel="stylesheet" href="css/notion-style.css">
255
+ </head>
256
+ <body class="auth-page">
257
+ <div class="auth-container">
258
+ <div class="auth-box">
259
+ <h1>You have been logged out</h1>
260
+ <p>Thank you for using ${siteName}.</p>
261
+ <a href="login.html" class="auth-button">Login Again</a>
262
+ </div>
263
+ </div>
264
+ </body>
265
+ </html>`;
266
+ }
267
+
268
+ /**
269
+ * Validate Supabase configuration
270
+ */
271
+ static validateConfig(config) {
272
+ const errors = [];
273
+
274
+ if (!config.auth.supabaseUrl) {
275
+ errors.push('auth.supabaseUrl is required');
276
+ }
277
+
278
+ if (!config.auth.supabaseAnonKey) {
279
+ errors.push('auth.supabaseAnonKey is required');
280
+ }
281
+
282
+ if (!config.auth.siteId) {
283
+ errors.push('auth.siteId is required');
284
+ }
285
+
286
+ // Validate URL format
287
+ if (config.auth.supabaseUrl && !config.auth.supabaseUrl.match(/^https:\/\/\w+\.supabase\.co$/)) {
288
+ errors.push('auth.supabaseUrl must be a valid Supabase URL (https://xxx.supabase.co)');
289
+ }
290
+
291
+ return errors;
292
+ }
293
+ }
294
+
295
+ module.exports = SupabaseAuth;
@@ -0,0 +1,191 @@
1
+ -- =====================================================
2
+ -- USER MANAGEMENT SQL COMMANDS FOR DOC-BUILDER
3
+ -- =====================================================
4
+ -- Run these in your Supabase SQL Editor
5
+
6
+ -- =====================================================
7
+ -- 1. VIEW YOUR SITES
8
+ -- =====================================================
9
+ -- See all your documentation sites
10
+ SELECT id, domain, name, created_at
11
+ FROM docbuilder_sites
12
+ ORDER BY created_at DESC;
13
+
14
+ -- =====================================================
15
+ -- 2. VIEW EXISTING USERS
16
+ -- =====================================================
17
+ -- See all users in your Supabase project
18
+ SELECT id, email, created_at, last_sign_in_at
19
+ FROM auth.users
20
+ ORDER BY created_at DESC;
21
+
22
+ -- =====================================================
23
+ -- 3. VIEW WHO HAS ACCESS TO A SPECIFIC SITE
24
+ -- =====================================================
25
+ -- Replace 'your-site-id' with actual site ID
26
+ SELECT
27
+ u.email,
28
+ u.id as user_id,
29
+ u.created_at as user_since,
30
+ da.created_at as access_granted,
31
+ ds.name as site_name,
32
+ ds.domain
33
+ FROM docbuilder_access da
34
+ JOIN auth.users u ON u.id = da.user_id
35
+ JOIN docbuilder_sites ds ON ds.id = da.site_id
36
+ WHERE da.site_id = 'your-site-id'
37
+ ORDER BY da.created_at DESC;
38
+
39
+ -- =====================================================
40
+ -- 4. ADD A SINGLE USER TO A SITE
41
+ -- =====================================================
42
+ -- First, create user in Supabase Dashboard (Authentication > Users > Invite)
43
+ -- Then grant access:
44
+ INSERT INTO docbuilder_access (user_id, site_id)
45
+ VALUES (
46
+ (SELECT id FROM auth.users WHERE email = 'user@example.com'),
47
+ 'your-site-id'
48
+ );
49
+
50
+ -- =====================================================
51
+ -- 5. ADD MULTIPLE USERS TO A SITE
52
+ -- =====================================================
53
+ -- Add a list of users all at once
54
+ WITH users_to_add AS (
55
+ SELECT email FROM (VALUES
56
+ ('user1@example.com'),
57
+ ('user2@example.com'),
58
+ ('user3@example.com'),
59
+ ('user4@example.com')
60
+ ) AS t(email)
61
+ )
62
+ INSERT INTO docbuilder_access (user_id, site_id)
63
+ SELECT u.id, 'your-site-id'
64
+ FROM auth.users u
65
+ JOIN users_to_add ua ON u.email = ua.email
66
+ WHERE NOT EXISTS (
67
+ -- Prevent duplicate entries
68
+ SELECT 1 FROM docbuilder_access
69
+ WHERE user_id = u.id AND site_id = 'your-site-id'
70
+ );
71
+
72
+ -- =====================================================
73
+ -- 6. GRANT USER ACCESS TO MULTIPLE SITES
74
+ -- =====================================================
75
+ -- Give one user access to several sites
76
+ WITH sites_to_grant AS (
77
+ SELECT id FROM docbuilder_sites
78
+ WHERE domain IN ('site1.com', 'site2.com', 'site3.com')
79
+ )
80
+ INSERT INTO docbuilder_access (user_id, site_id)
81
+ SELECT
82
+ (SELECT id FROM auth.users WHERE email = 'user@example.com'),
83
+ s.id
84
+ FROM sites_to_grant s
85
+ WHERE NOT EXISTS (
86
+ SELECT 1 FROM docbuilder_access
87
+ WHERE user_id = (SELECT id FROM auth.users WHERE email = 'user@example.com')
88
+ AND site_id = s.id
89
+ );
90
+
91
+ -- =====================================================
92
+ -- 7. REMOVE USER ACCESS FROM A SITE
93
+ -- =====================================================
94
+ -- Remove specific user from specific site
95
+ DELETE FROM docbuilder_access
96
+ WHERE user_id = (SELECT id FROM auth.users WHERE email = 'user@example.com')
97
+ AND site_id = 'your-site-id';
98
+
99
+ -- =====================================================
100
+ -- 8. REMOVE USER FROM ALL SITES
101
+ -- =====================================================
102
+ -- Completely remove user's access to all documentation
103
+ DELETE FROM docbuilder_access
104
+ WHERE user_id = (SELECT id FROM auth.users WHERE email = 'user@example.com');
105
+
106
+ -- =====================================================
107
+ -- 9. BULK REMOVE USERS
108
+ -- =====================================================
109
+ -- Remove multiple users from a site
110
+ WITH users_to_remove AS (
111
+ SELECT email FROM (VALUES
112
+ ('olduser1@example.com'),
113
+ ('olduser2@example.com')
114
+ ) AS t(email)
115
+ )
116
+ DELETE FROM docbuilder_access
117
+ WHERE site_id = 'your-site-id'
118
+ AND user_id IN (
119
+ SELECT u.id FROM auth.users u
120
+ JOIN users_to_remove ur ON u.email = ur.email
121
+ );
122
+
123
+ -- =====================================================
124
+ -- 10. VIEW ACCESS SUMMARY
125
+ -- =====================================================
126
+ -- See how many users each site has
127
+ SELECT
128
+ ds.name as site_name,
129
+ ds.domain,
130
+ ds.id as site_id,
131
+ COUNT(da.user_id) as user_count,
132
+ MAX(da.created_at) as last_access_granted
133
+ FROM docbuilder_sites ds
134
+ LEFT JOIN docbuilder_access da ON ds.id = da.site_id
135
+ GROUP BY ds.id, ds.name, ds.domain
136
+ ORDER BY user_count DESC;
137
+
138
+ -- =====================================================
139
+ -- 11. FIND USERS WITHOUT ACCESS TO ANY SITE
140
+ -- =====================================================
141
+ -- Useful for cleanup
142
+ SELECT u.email, u.created_at, u.last_sign_in_at
143
+ FROM auth.users u
144
+ WHERE NOT EXISTS (
145
+ SELECT 1 FROM docbuilder_access da
146
+ WHERE da.user_id = u.id
147
+ )
148
+ ORDER BY u.created_at DESC;
149
+
150
+ -- =====================================================
151
+ -- 12. AUDIT LOG - RECENT ACCESS GRANTS
152
+ -- =====================================================
153
+ -- See who was granted access recently
154
+ SELECT
155
+ u.email,
156
+ ds.name as site_name,
157
+ ds.domain,
158
+ da.created_at as access_granted
159
+ FROM docbuilder_access da
160
+ JOIN auth.users u ON u.id = da.user_id
161
+ JOIN docbuilder_sites ds ON ds.id = da.site_id
162
+ WHERE da.created_at > NOW() - INTERVAL '30 days'
163
+ ORDER BY da.created_at DESC;
164
+
165
+ -- =====================================================
166
+ -- 13. COPY ACCESS FROM ONE USER TO ANOTHER
167
+ -- =====================================================
168
+ -- Useful when replacing team members
169
+ INSERT INTO docbuilder_access (user_id, site_id)
170
+ SELECT
171
+ (SELECT id FROM auth.users WHERE email = 'newuser@example.com'),
172
+ da.site_id
173
+ FROM docbuilder_access da
174
+ WHERE da.user_id = (SELECT id FROM auth.users WHERE email = 'olduser@example.com');
175
+
176
+ -- =====================================================
177
+ -- COMMON SITE IDs FOR REFERENCE
178
+ -- =====================================================
179
+ -- Your test site: 4d8a53bf-dcdd-48c0-98e0-cd1451518735
180
+ -- Add more site IDs here as you create them:
181
+ -- Production site: xxx
182
+ -- Staging site: xxx
183
+
184
+ -- =====================================================
185
+ -- TIPS
186
+ -- =====================================================
187
+ -- 1. Always check if users exist before granting access
188
+ -- 2. Use the Supabase Dashboard to invite new users first
189
+ -- 3. Run SELECT queries before DELETE to verify
190
+ -- 4. Keep this file updated with your site IDs
191
+ -- 5. Consider creating views for common queries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowcode/doc-builder",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "Reusable documentation builder for markdown-based sites with Vercel deployment support",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "homepage": "https://github.com/wapdat/doc-builder#readme",
33
33
  "dependencies": {
34
34
  "@knowcode/doc-builder": "^1.4.21",
35
+ "@supabase/supabase-js": "^2.39.0",
35
36
  "chalk": "^4.1.2",
36
37
  "commander": "^11.0.0",
37
38
  "fs-extra": "^11.2.0",
@@ -0,0 +1,22 @@
1
+ module.exports = {
2
+ siteName: '✨ Test Documentation',
3
+ siteDescription: 'Public documentation site',
4
+ favicon: '✨',
5
+ footer: {
6
+ copyright: 'Test Documentation',
7
+ links: []
8
+ },
9
+ docsDir: 'test-docs',
10
+ outputDir: 'public-html',
11
+ // No authentication configured - this is a public site
12
+ features: {
13
+ authentication: false,
14
+ darkMode: true,
15
+ mermaid: true,
16
+ changelog: false,
17
+ normalizeTitle: true,
18
+ showPdfDownload: true,
19
+ menuDefaultOpen: true,
20
+ phosphorIcons: true
21
+ }
22
+ };
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Page Not Found - Redirecting...</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
+ margin: 0;
12
+ padding: 0;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ min-height: 100vh;
17
+ background-color: #f7f7f5;
18
+ color: #37352f;
19
+ }
20
+ .container {
21
+ text-align: center;
22
+ padding: 2rem;
23
+ max-width: 600px;
24
+ }
25
+ h1 {
26
+ font-size: 3rem;
27
+ margin: 0 0 1rem 0;
28
+ color: #37352f;
29
+ }
30
+ p {
31
+ font-size: 1.125rem;
32
+ line-height: 1.6;
33
+ color: #6b6b6b;
34
+ margin: 0 0 2rem 0;
35
+ }
36
+ a {
37
+ color: #0366d6;
38
+ text-decoration: none;
39
+ }
40
+ a:hover {
41
+ text-decoration: underline;
42
+ }
43
+ .emoji {
44
+ font-size: 4rem;
45
+ margin-bottom: 1rem;
46
+ }
47
+ .loading {
48
+ display: none;
49
+ color: #0366d6;
50
+ margin-top: 1rem;
51
+ }
52
+ .redirect-message {
53
+ display: none;
54
+ background-color: #e8f4fd;
55
+ border: 1px solid #c3e0f7;
56
+ padding: 1rem;
57
+ border-radius: 8px;
58
+ margin-top: 1rem;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <div class="emoji">🔍</div>
65
+ <h1>404</h1>
66
+ <p id="message">The page you're looking for doesn't exist.</p>
67
+ <div id="redirect-message" class="redirect-message">
68
+ Redirecting to the correct page...
69
+ </div>
70
+ <p id="loading" class="loading">Redirecting...</p>
71
+ <p>
72
+ <a href="/" id="home-link">Go to Home</a>
73
+ </p>
74
+ </div>
75
+
76
+ <script>
77
+ // Check if the URL ends with .md
78
+ const pathname = window.location.pathname;
79
+
80
+ if (pathname.endsWith('.md')) {
81
+ // Convert .md to .html
82
+ const htmlPath = pathname.replace(/\.md$/, '.html');
83
+
84
+ // Show redirect message
85
+ document.getElementById('message').textContent = 'Found a markdown link. Redirecting to the HTML version...';
86
+ document.getElementById('redirect-message').style.display = 'block';
87
+ document.getElementById('loading').style.display = 'block';
88
+
89
+ // Redirect after a brief delay to show the message
90
+ setTimeout(() => {
91
+ window.location.replace(htmlPath);
92
+ }, 500);
93
+ } else {
94
+ // For true 404s, show the standard message
95
+ document.getElementById('message').textContent = "The page you're looking for doesn't exist.";
96
+
97
+ // Also check if we can suggest a similar page
98
+ // Remove common suffixes and try to find a match
99
+ const basePath = pathname
100
+ .replace(/\.(html|htm|php|asp|aspx)$/, '')
101
+ .replace(/\/$/, '');
102
+
103
+ if (basePath && basePath !== pathname) {
104
+ document.getElementById('message').innerHTML =
105
+ `The page you're looking for doesn't exist.<br>
106
+ <small>Did you mean <a href="${basePath}.html">${basePath}.html</a>?</small>`;
107
+ }
108
+ }
109
+
110
+ // Update home link to use the correct base URL
111
+ const baseUrl = window.location.origin;
112
+ document.getElementById('home-link').href = baseUrl;
113
+ </script>
114
+ </body>
115
+ </html>