interactive intro to open social at-me.zzstoatzz.io

fix: improve domain validation for filter functionality

- Make renderVisualization async and await validateAppUrls to ensure
validation completes before filter panel initializes
- Rewrite validateAppUrls with two-phase approach:
1. Check if domain resolves as ATProto handle (fast, no CORS issues)
2. Fall back to HEAD request for non-ATProto domains
- Mark domains as invalid on any fetch error (not just Chrome-specific
error strings) for cross-browser compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+32 -23
+27 -18
src/view/atproto.js
··· 152 const url = link?.dataset.url; 153 if (!url || !link) return; 154 155 try { 156 - new URL(url); // Check syntax first 157 } catch (e) { 158 markInvalid(link, namespace, 'malformed URL'); 159 return; 160 } 161 162 - // Try HEAD request with short timeout to check if domain is reachable 163 try { 164 - const controller = new AbortController(); 165 - const timeout = setTimeout(() => controller.abort(), 3000); 166 167 await fetch(url, { 168 method: 'HEAD', 169 mode: 'no-cors', 170 - signal: controller.signal, 171 }); 172 - 173 - clearTimeout(timeout); 174 - // If we get here, domain is reachable (even if response is opaque due to CORS) 175 } catch (e) { 176 - // Only mark as invalid for actual DNS/connection failures 177 - // CORS blocks mean the server IS reachable, just not allowing our request 178 - const errorMsg = e.message || ''; 179 - if (errorMsg.includes('ERR_NAME_NOT_RESOLVED') || 180 - errorMsg.includes('ERR_CONNECTION_REFUSED') || 181 - errorMsg.includes('ERR_CONNECTION_TIMED_OUT') || 182 - e.name === 'AbortError') { 183 - markInvalid(link, namespace, 'domain not reachable'); 184 - } 185 - // For CORS blocks (ERR_FAILED) and other errors, server exists so don't mark invalid 186 } 187 }); 188
··· 152 const url = link?.dataset.url; 153 if (!url || !link) return; 154 155 + let parsedUrl; 156 try { 157 + parsedUrl = new URL(url); 158 } catch (e) { 159 markInvalid(link, namespace, 'malformed URL'); 160 return; 161 } 162 163 + // Use a two-phase approach: 164 + // 1. Try to resolve the domain as an ATProto handle (fast, no CORS issues) 165 + // 2. If that fails, try a HEAD request to check if domain exists 166 + 167 + const domain = parsedUrl.hostname; 168 + 169 + // Phase 1: Check if domain resolves as ATProto handle 170 try { 171 + const response = await fetch( 172 + `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(domain)}`, 173 + { signal: AbortSignal.timeout(3000) } 174 + ); 175 + if (response.ok) { 176 + // Domain exists as ATProto handle - definitely valid 177 + return; 178 + } 179 + } catch (e) { 180 + // Continue to phase 2 181 + } 182 183 + // Phase 2: Try HEAD request with no-cors to check domain reachability 184 + try { 185 await fetch(url, { 186 method: 'HEAD', 187 mode: 'no-cors', 188 + signal: AbortSignal.timeout(3000), 189 }); 190 + // If we get here without throwing, domain is reachable 191 } catch (e) { 192 + // Any fetch error means domain is not reachable 193 + // (TypeError for network errors, AbortError for timeout) 194 + markInvalid(link, namespace, 'domain not reachable'); 195 } 196 }); 197
+2 -2
src/view/main.js
··· 129 // Hide status 130 statusEl.style.display = 'none'; 131 132 - // Render visualization 133 - renderVisualization(apps, profile); 134 135 // Initialize UI components 136 initGuestbookUI();
··· 129 // Hide status 130 statusEl.style.display = 'none'; 131 132 + // Render visualization (await to ensure URL validation completes) 133 + await renderVisualization(apps, profile); 134 135 // Initialize UI components 136 initGuestbookUI();
+3 -3
src/view/visualization.js
··· 13 import { initFilterPanel, repositionAppCircles } from './filters.js'; 14 import { loadMSTStructure } from './mst.js'; 15 16 - export function renderVisualization(apps, profile) { 17 const field = document.getElementById('field'); 18 field.innerHTML = ''; 19 field.classList.remove('loading'); ··· 109 }); 110 }); 111 112 - // Validate app URLs (client-side check via image load) 113 - validateAppUrls(appDivs); 114 115 // Set up identity click handler 116 setupIdentityClickHandler(allCollections, appCount, profile);
··· 13 import { initFilterPanel, repositionAppCircles } from './filters.js'; 14 import { loadMSTStructure } from './mst.js'; 15 16 + export async function renderVisualization(apps, profile) { 17 const field = document.getElementById('field'); 18 field.innerHTML = ''; 19 field.classList.remove('loading'); ··· 109 }); 110 }); 111 112 + // Validate app URLs (must complete before filter panel setup) 113 + await validateAppUrls(appDivs); 114 115 // Set up identity click handler 116 setupIdentityClickHandler(allCollections, appCount, profile);